diff --git a/client/frame-runner.js b/client/frame-runner.js index 3411a790a02..3f995abce01 100644 --- a/client/frame-runner.js +++ b/client/frame-runner.js @@ -7,6 +7,28 @@ document.addEventListener('DOMContentLoaded', function() { var source = document.__source; var __getUserInput = document.__getUserInput || (x => x); var checkChallengePayload = document.__checkChallengePayload; + + // Fake Deep Equal dependency + /* eslint-disable no-unused-vars */ + const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); + + // Hardcode Deep Freeze dependency + var DeepFreeze = (o) => { + Object.freeze(o); + Object.getOwnPropertyNames(o).forEach(function(prop) { + if (o.hasOwnProperty(prop) + && o[prop] !== null + && ( + typeof o[prop] === 'object' || + typeof o[prop] === 'function' + ) + && !Object.isFrozen(o[prop])) { + DeepFreeze(o[prop]); + } + }); + return o; + }; + if (document.Enzyme) { window.Enzyme = document.Enzyme; } @@ -29,8 +51,10 @@ document.addEventListener('DOMContentLoaded', function() { document.__runTests = function runTests(tests = []) { /* eslint-disable no-unused-vars */ - const editor = { getValue() { return source; } }; const code = source; + const editor = { + getValue() { return source; } + }; /* eslint-enable no-unused-vars */ if (window.__err) { return Rx.Observable.from(tests) diff --git a/common/app/routes/Challenges/rechallenge/builders.js b/common/app/routes/Challenges/rechallenge/builders.js index 31ac1ec34bf..e0764403833 100644 --- a/common/app/routes/Challenges/rechallenge/builders.js +++ b/common/app/routes/Challenges/rechallenge/builders.js @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { Observable } from 'rx'; import cond from 'lodash/cond'; import flow from 'lodash/flow'; @@ -18,6 +19,13 @@ import { const htmlCatch = '\n\n'; const jsCatch = '\n;/*fcc*/\n'; +const defaultTemplate = ({ source }) => ` +
+ + ${source} + + +`; const wrapInScript = partial(transformContents, (content) => ( `${htmlCatch}` @@ -46,9 +54,11 @@ export const cssToHtml = cond([ ]); // FileStream::concactHtml( -// required: [ ...Object ] +// required: [ ...Object ], +// template: String // ) => Observable[{ build: String, sources: Dictionary }] -export function concactHtml(required) { +export function concactHtml(required, template) { + const createBody = template ? _.template(template) : defaultTemplate; const source = this.shareReplay(); const sourceMap = source .flatMap(files => files.reduce((sources, file) => { @@ -73,13 +83,8 @@ export function concactHtml(required) { .flatMap(file => file.reduce((body, file) => { return body + file.contents + htmlCatch; }, '')) - .map(source => ` - - - ${source} - - - `); + .map(source => ({ source })) + .map(createBody); return Observable .combineLatest( diff --git a/common/app/routes/Challenges/rechallenge/transformers.js b/common/app/routes/Challenges/rechallenge/transformers.js index 6b59057b8b3..5eb60d5c8ef 100644 --- a/common/app/routes/Challenges/rechallenge/transformers.js +++ b/common/app/routes/Challenges/rechallenge/transformers.js @@ -9,6 +9,7 @@ import * as vinyl from '../../../../utils/polyvinyl.js'; import castToObservable from '../../../utils/cast-to-observable.js'; const babelOptions = { presets: [ presetEs2015, presetReact ] }; +const babelTransformCode = code => babel.transform(code, babelOptions).code; function loopProtectHit(line) { var err = 'Exiting potential infinite loop at line ' + line + @@ -81,8 +82,8 @@ export const babelTransformer = _.cond([ testJS$JSX, _.flow( _.partial( - vinyl.transformContents, - contents => babel.transform(contents, babelOptions).code + vinyl.transformHeadTailAndContents, + babelTransformCode ), _.partial(vinyl.setExt, 'js') ) diff --git a/common/app/routes/Challenges/redux/execute-challenge-epic.js b/common/app/routes/Challenges/redux/execute-challenge-epic.js index c7ac212f3b9..10e24f92693 100644 --- a/common/app/routes/Challenges/redux/execute-challenge-epic.js +++ b/common/app/routes/Challenges/redux/execute-challenge-epic.js @@ -29,7 +29,6 @@ import { challengeSelector } from '../../../redux'; -import { filesSelector } from '../../../files'; const executeDebounceTimeout = 750; export function updateMainEpic(actions, { getState }, { document }) { @@ -53,15 +52,11 @@ export function updateMainEpic(actions, { getState }, { document }) { !codeLockedSelector(getState()) && showPreviewSelector(getState()) )) - .map(getState) - .flatMapLatest(state => { - const files = filesSelector(state); - const { required = [] } = challengeSelector(state); - return buildFromFiles(files, required, true) - .map(frameMain) - .ignoreElements() - .catch(createErrorObservable); - }); + .flatMapLatest(() => buildFromFiles(getState(), true) + .map(frameMain) + .ignoreElements() + .catch(createErrorObservable) + ); return Observable.merge(buildAndFrameMain, proxyLogger.map(updateOutput)); }); } @@ -91,7 +86,7 @@ export function executeChallengeEpic(actions, { getState }, { document }) { .flatMap(tests => { return Observable.from(tests) .map(({ message }) => message) - // make sure that the test message is a non empty string + // make sure that the test message is a non empty string .filter(_.overEvery(_.isString, Boolean)) .map(updateOutput) .concat(Observable.of(updateTests(tests))); @@ -104,11 +99,7 @@ export function executeChallengeEpic(actions, { getState }, { document }) { .filter(() => !codeLockedSelector(getState())) .flatMapLatest(() => { const state = getState(); - const files = filesSelector(state); - const { - required = [], - type: challengeType - } = challengeSelector(state); + const { type: challengeType } = challengeSelector(state); if (challengeType === 'backend') { return buildBackendChallenge(state) .do(frameTests) @@ -116,7 +107,7 @@ export function executeChallengeEpic(actions, { getState }, { document }) { .startWith(initOutput('// running test')) .catch(createErrorObservable); } - return buildFromFiles(files, required, false) + return buildFromFiles(state, false) .do(frameTests) .ignoreElements() .startWith(initOutput('// running test')) diff --git a/common/app/routes/Challenges/redux/index.js b/common/app/routes/Challenges/redux/index.js index d56301e85b9..e7587062879 100644 --- a/common/app/routes/Challenges/redux/index.js +++ b/common/app/routes/Challenges/redux/index.js @@ -207,6 +207,8 @@ export const challengeModalSelector = export const bugModalSelector = state => getNS(state).isBugOpen; +export const challengeRequiredSelector = state => + challengeSelector(state).required || []; export const challengeMetaSelector = createSelector( // use closure to get around circular deps (...args) => challengeSelector(...args), @@ -223,6 +225,7 @@ export const challengeMetaSelector = createSelector( challenge.title; return { + type, title, viewType, submitType: @@ -242,6 +245,10 @@ export const challengeMetaSelector = createSelector( export const showPreviewSelector = state => !!challengeMetaSelector(state).showPreview; +export const challengeTypeSelector = state => + challengeMetaSelector(state).type || ''; +export const challengeTemplateSelector = state => + challengeSelector(state).template || null; export default combineReducers( handleActions( diff --git a/common/app/routes/Challenges/utils/build.js b/common/app/routes/Challenges/utils/build.js index a31ed9339ee..aff251e5f9d 100644 --- a/common/app/routes/Challenges/utils/build.js +++ b/common/app/routes/Challenges/utils/build.js @@ -4,6 +4,7 @@ import identity from 'lodash/identity'; import { fetchScript } from './fetch-and-cache.js'; import throwers from '../rechallenge/throwers'; +import { challengeTemplateSelector, challengeRequiredSelector } from '../redux'; import { applyTransformers, proxyLoggerTransformer @@ -13,6 +14,9 @@ import { jsToHtml, concactHtml } from '../rechallenge/builders.js'; + +import { filesSelector } from '../../../files'; + import { createFileStream, pipe @@ -35,7 +39,9 @@ const globalRequires = [ jQuery ]; -export function buildFromFiles(files, required, shouldProxyConsole) { +export function buildFromFiles(state, shouldProxyConsole) { + const files = filesSelector(state); + const required = challengeRequiredSelector(state); const finalRequires = [...globalRequires, ...required ]; return createFileStream(files) ::pipe(throwers) @@ -43,7 +49,7 @@ export function buildFromFiles(files, required, shouldProxyConsole) { ::pipe(shouldProxyConsole ? proxyLoggerTransformer : identity) ::pipe(jsToHtml) ::pipe(cssToHtml) - ::concactHtml(finalRequires, frameRunner); + ::concactHtml(finalRequires, challengeTemplateSelector(state)); } export function buildBackendChallenge(state) { diff --git a/common/app/routes/Challenges/utils/index.js b/common/app/routes/Challenges/utils/index.js index 7f3edc63a2b..a10c79a92c8 100644 --- a/common/app/routes/Challenges/utils/index.js +++ b/common/app/routes/Challenges/utils/index.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import * as challengeTypes from '../../../utils/challengeTypes.js'; -import { createPoly } from '../../../../utils/polyvinyl.js'; +import { createPoly, updateFileFromSpec } from '../../../../utils/polyvinyl.js'; import { decodeScriptTags } from '../../../../utils/encode-decode.js'; // turn challengeType to file ext @@ -82,8 +82,8 @@ export function challengeToFiles(challenge, files) { files = files || challenge.files || {}; if (challenge.type === 'modern') { return _.reduce(files, (files, file) => { - // TODO(berks): need to make sure head/tail are fresh from fCC - files[file.key] = createPoly(file); + const challengeSpec = _.get(challenge, ['files', file.key]) || {}; + files[file.key] = updateFileFromSpec(challengeSpec, file); return files; }, {}); } diff --git a/common/models/challenge.json b/common/models/challenge.json index 0126e540ac8..e416ae43926 100644 --- a/common/models/challenge.json +++ b/common/models/challenge.json @@ -130,6 +130,10 @@ } ], "default": [] + }, + "template": { + "type": "string", + "description": "A template to render the compiled challenge source into. This template uses template literal delimiter, i.e. ${ foo }" } }, "validations": [], diff --git a/common/utils/polyvinyl.js b/common/utils/polyvinyl.js index 460abc8e059..cd5dd9aeda8 100644 --- a/common/utils/polyvinyl.js +++ b/common/utils/polyvinyl.js @@ -198,8 +198,8 @@ export function transformContents(wrap, poly) { // ) => PolyVinyl export function transformHeadTailAndContents(wrap, poly) { return { - ...setContent( - wrap(poly.contents), + ...transformContents( + wrap, poly ), head: wrap(poly.head), @@ -210,3 +210,7 @@ export function transformHeadTailAndContents(wrap, poly) { export function testContents(predicate, poly) { return !!predicate(poly.contents); } + +export function updateFileFromSpec(spec, poly) { + return setContent(poly.contents, createPoly(spec)); +} diff --git a/package-lock.json b/package-lock.json index fd2032ed105..269a22ef819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -143,6 +143,12 @@ "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=" }, + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -414,6 +420,12 @@ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", "dev": true }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -1727,6 +1739,11 @@ "regenerator-runtime": "0.11.0" } }, + "babel-standalone": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-standalone/-/babel-standalone-6.26.0.tgz", + "integrity": "sha1-Ffs9NfLEVmlYFevx7Zb+fwFbaIY=" + }, "babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", @@ -1998,6 +2015,12 @@ "repeat-element": "1.1.2" } }, + "browser-process-hrtime": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", + "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=", + "dev": true + }, "browser-sync": { "version": "2.18.13", "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.18.13.tgz", @@ -3261,6 +3284,12 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "content-type-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz", + "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==", + "dev": true + }, "conventional-commit-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz", @@ -3608,6 +3637,21 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" }, + "cssom": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", + "dev": true + }, + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "dev": true, + "requires": { + "cssom": "0.3.2" + } + }, "csurf": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz", @@ -3984,6 +4028,12 @@ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" }, + "domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.0.tgz", + "integrity": "sha512-WpwuBlZ2lQRFa4H/4w49deb9rJLot9KmqrKKjMc9qBl7CID+DdC2swoa34ccRl+anL2B6bLp6TjFdIdnzekMBQ==", + "dev": true + }, "domhandler": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", @@ -7921,6 +7971,15 @@ "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.1.0.tgz", "integrity": "sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA==" }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "1.0.3" + } + }, "html-entities": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", @@ -8789,6 +8848,74 @@ "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==", "dev": true }, + "jsdom": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.5.1.tgz", + "integrity": "sha512-89ztIZ03aYK9f1uUrLXLsZndRge/JnZjzjpaN+lrse3coqz+8PR/dX4WLHpbF5fIKTXhDjFODOJw2328lPJ90g==", + "dev": true, + "requires": { + "abab": "1.0.4", + "acorn": "5.2.1", + "acorn-globals": "4.1.0", + "array-equal": "1.0.0", + "browser-process-hrtime": "0.1.2", + "content-type-parser": "1.0.2", + "cssom": "0.3.2", + "cssstyle": "0.2.37", + "domexception": "1.0.0", + "escodegen": "1.9.0", + "html-encoding-sniffer": "1.0.2", + "left-pad": "1.2.0", + "nwmatcher": "1.4.3", + "parse5": "3.0.3", + "pn": "1.0.0", + "request": "2.83.0", + "request-promise-native": "1.0.5", + "sax": "1.2.4", + "symbol-tree": "3.2.2", + "tough-cookie": "2.3.3", + "webidl-conversions": "4.0.2", + "whatwg-encoding": "1.0.3", + "whatwg-url": "6.4.0", + "xml-name-validator": "2.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", + "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==", + "dev": true + }, + "acorn-globals": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", + "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", + "dev": true, + "requires": { + "acorn": "5.2.1" + } + }, + "escodegen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.0.tgz", + "integrity": "sha512-v0MYvNQ32bzwoG2OSFzWAkuahDQHK92JBN0pTAALJ4RIxEZe766QJPDR8Hqy7XNUy5K3fnVL76OqYAdc4TZEIw==", + "dev": true, + "requires": { + "esprima": "3.1.3", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.5.7" + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + } + } + }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", @@ -9042,6 +9169,12 @@ "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", "dev": true }, + "left-pad": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.2.0.tgz", + "integrity": "sha1-0wpzxrggHY99jnlWupYWCHpo4O4=", + "dev": true + }, "less": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", @@ -10048,6 +10181,12 @@ "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, "lodash.template": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", @@ -12008,6 +12147,12 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "nwmatcher": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.3.tgz", + "integrity": "sha512-IKdSTiDWCarf2JTS5e9e2+5tPZGdkRJ79XjYV0pzK8Q9BpsFyBq1RGKxzs7Q8UBushGw7m6TzVKz6fcY99iSWw==", + "dev": true + }, "oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -12847,6 +12992,12 @@ "json-stringify-safe": "5.0.1" } }, + "pn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.0.0.tgz", + "integrity": "sha1-HPWjCw2AbNGPiPxBprXUrWFbO6k=", + "dev": true + }, "portscanner": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.1.1.tgz", @@ -13611,6 +13762,12 @@ "react-lazy-cache": "3.0.1" } }, + "redux-thunk": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz", + "integrity": "sha1-5hWhbha0ehmlFXZhM9Hj6Zt4UuU=", + "dev": true + }, "referrer-policy": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz", @@ -13797,6 +13954,26 @@ "uuid": "3.1.0" } }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "dev": true, + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "1.1.1", + "tough-cookie": "2.3.3" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -15311,6 +15488,12 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, "store": { "version": "git+https://github.com/berkeleytrue/store.js.git#562c990c2f8018b6401a640aada8e9f667554fe1" }, @@ -15751,6 +15934,12 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "dev": true + }, "table": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", @@ -16209,6 +16398,23 @@ "punycode": "1.4.1" } }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=", + "dev": true + } + } + }, "transformers": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", @@ -16950,6 +17156,12 @@ } } }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, "webpack": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.15.0.tgz", @@ -17256,11 +17468,31 @@ } } }, + "whatwg-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", + "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.19" + } + }, "whatwg-fetch": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" }, + "whatwg-url": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.4.0.tgz", + "integrity": "sha512-Z0CVh/YE217Foyb488eo+iBv+r7eAQ0wSTyApi9n06jhcA3z6Nidg/EGvl0UFkg7kMdKxfBzzr+o9JF+cevgMg==", + "dev": true, + "requires": { + "lodash.sortby": "4.7.0", + "tr46": "1.0.1", + "webidl-conversions": "4.0.2" + } + }, "when": { "version": "3.7.8", "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz", @@ -17406,6 +17638,12 @@ "xtend": "4.0.1" } }, + "xml-name-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", + "dev": true + }, "xml2js": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", diff --git a/package.json b/package.json index 9de126ab01c..bcddac7dd8c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "babel-register": "^6.3.0", + "babel-standalone": "^6.26.0", "berkeleys-redux-utils": "^4.0.0", "body-parser": "^1.13.2", "bootstrap": "~3.3.7", @@ -169,12 +170,14 @@ "gulp-util": "^3.0.6", "husky": "^0.14.3", "istanbul-coveralls": "^1.0.3", + "jsdom": "^11.5.1", "json-loader": "~0.5.2", "less": "^2.5.1", "loopback-component-explorer": "^2.1.1", "merge-stream": "^1.0.0", "proxyquire": "^1.7.10", "react-hot-loader": "^1.3.0", + "redux-thunk": "^2.2.0", "remote-redux-devtools": "^0.5.12", "rev-del": "^1.0.5", "sinon": "^2.0.0", diff --git a/seed/challenges/03-front-end-libraries/react-and-redux.json b/seed/challenges/03-front-end-libraries/react-and-redux.json index 86635fe32c6..020aef9d41b 100644 --- a/seed/challenges/03-front-end-libraries/react-and-redux.json +++ b/seed/challenges/03-front-end-libraries/react-and-redux.json @@ -3,25 +3,758 @@ "order": 7, "time": "5 hours", "helpRoom": "Help", + "required": [ + { + "src": "https://cdnjs.cloudflare.com/ajax/libs/react/16.1.1/umd/react.development.js" + }, + { + "src": "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.1.1/umd/react-dom.development.js" + }, + { + "src": "https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js" + }, + { + "src": "https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js" + } + ], + "template": "${ source }", "challenges": [ { - "id": "587d7dbc367417b2b2512bb0", - "title": "Introduction to the React and Redux Challenges", + "id": "5a24c314108439a4d4036141", + "title": "Getting Started with React Redux", + "releasedOn": "December 25, 2017", "description": [ - [ - "", - "", - "The React and Redux challenges have not been ported into freeCodeCamp yet. You can visit this link to work through the alpha version of these challenges. If you have feedback, you can open an issue (or pull request) directly on this repository.", - "" - ] + "This series of challenges introduces how to use Redux with React. First, here's a review of some of the key principles of each technology. React is a view library that you provide with data, then it renders the view in an efficient, predictable way. Redux is a state management framework that you can use to simplify the management of your application's state. Typically, in a React Redux app, you create a single Redux store that manages the state of your entire app. Your React components subscribe to only the pieces of data in the store that are relevant to their role. Then, you dispatch actions directly from React components, which then trigger store updates.", + "Although React components can manage their own state locally, when you have a complex app, it's generally better to keep the app state in a single location with Redux. There are exceptions when individual components may have local state specific only to them. Finally, because Redux is not designed to work with React out of the box, you need to use thereact-redux package. It provides a way for you to pass Redux state and dispatch to your React components as props.",
+ "Over the next few challenges, first, you'll create a simple React component which allows you to input new text messages. These are added to an array that's displayed in the view. This should be a nice review of what you learned in the React lessons. Next, you'll create a Redux store and actions that manage the state of the messages array. Finally, you'll use react-redux to connect the Redux store with your component, thereby extracting the local state into the Redux store.",
+ "DisplayMessages component. Add a constructor to this component and initialize it with a state that has two properties: input, that's set to an empty string, and messages, that's set to an empty array."
],
- "releasedOn": "Feb 17, 2017",
- "challengeSeed": [],
- "tests": [],
- "type": "waypoint",
- "challengeType": 7,
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "class DisplayMessages extends React.Component {",
+ " // change code below this line",
+ "",
+ " // change code above this line",
+ " render() {",
+ " return ",
+ " }",
+ "};"
+ ],
+ "tail": "ReactDOM.render(DisplayMessages component should render an empty div element.');",
+ "getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/ /g,''); return noWhiteSpace.includes('constructor(props)') && noWhiteSpace.includes('super(props'); })(), 'message: The DisplayMessages constructor should be called properly with super, passing in props.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); const initialState = mockedComponent.state(); return typeof initialState === 'object' && initialState.input === '' && Array.isArray(initialState.messages) && initialState.messages.length === 0; })(), 'message: The DisplayMessages component should have an initial state equal to {input: \"\", messages: []}.');"
+ ],
+ "solutions": [
+ "class DisplayMessages extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n input: '',\n messages: []\n }\n }\n render() {\n return \n }\n};"
+ ],
+ "type": "modern",
"isRequired": false,
- "translations": {}
+ "translations": {},
+ "react": true
+ },
+ {
+ "id": "5a24c314108439a4d4036142",
+ "title": "Manage State Locally First",
+ "releasedOn": "December 25, 2017",
+ "description": [
+ "Here you'll finish creating the DisplayMessages component.",
+ "render() method, have the component render an input element, button element, and ul element. When the input element changes, it should trigger a handleChange() method. Also, the input element should render the value of input that's in the component's state. The button element should trigger a submitMessage() method when it's clicked.",
+ "Second, write these two methods. The handleChange() method should update the input with what the user is typing. The submitMessage() method should concatenate the current message (stored in input) to the messages array in local state, and clear the value of the input.",
+ "Finally, use the ul to map over the array of messages and render it to the screen as a list of li elements."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "class DisplayMessages extends React.Component {",
+ " constructor(props) {",
+ " super(props);",
+ " this.state = {",
+ " input: '',",
+ " messages: []",
+ " }",
+ " }",
+ " // add handleChange() and submitMessage() methods here",
+ "",
+ " render() {",
+ " return (",
+ " DisplayMessages component should initialize with a state equal to { input: \"\", messages: [] }.');",
+ "async () => { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); const state = () => { mockedComponent.setState({messages: ['__TEST__MESSAGE']}); return waitForIt(() => mockedComponent )}; const updated = await state(); assert(updated.find('div').length === 1 && updated.find('h2').length === 1 && updated.find('button').length === 1 && updated.find('ul').length === 1, 'message: The DisplayMessages component should render a div containing an h2 element, a button element, a ul element, and li elements as children.'); }; ",
+ "async () => { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); const causeChange = (c, v) => c.find('input').simulate('change', { target: { value: v }}); const testValue = '__TEST__EVENT__INPUT'; const changed = () => { causeChange(mockedComponent, testValue); return waitForIt(() => mockedComponent )}; const updated = await changed(); assert(updated.find('input').props().value === testValue, 'message: The input element should render the value of input in local state.'); }; ",
+ "async () => { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); const causeChange = (c, v) => c.find('input').simulate('change', { target: { value: v }}); const initialState = mockedComponent.state(); const testMessage = '__TEST__EVENT__MESSAGE__'; const changed = () => { causeChange(mockedComponent, testMessage); return waitForIt(() => mockedComponent )}; const afterInput = await changed(); assert(initialState.input === '' && afterInput.state().input === '__TEST__EVENT__MESSAGE__', 'message: Calling the method handleChange should update the input value in state to the current input.'); }; ",
+ "async () => { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); const causeChange = (c, v) => c.find('input').simulate('change', { target: { value: v }}); const initialState = mockedComponent.state(); const testMessage_1 = '__FIRST__MESSAGE__'; const firstChange = () => { causeChange(mockedComponent, testMessage_1); return waitForIt(() => mockedComponent )}; const firstResult = await firstChange(); const firstSubmit = () => { mockedComponent.find('button').simulate('click'); return waitForIt(() => mockedComponent )}; const afterSubmit_1 = await firstSubmit(); const submitState_1 = afterSubmit_1.state(); const testMessage_2 = '__SECOND__MESSAGE__'; const secondChange = () => { causeChange(mockedComponent, testMessage_2); return waitForIt(() => mockedComponent )}; const secondResult = await secondChange(); const secondSubmit = () => { mockedComponent.find('button').simulate('click'); return waitForIt(() => mockedComponent )}; const afterSubmit_2 = await secondSubmit(); const submitState_2 = afterSubmit_2.state(); assert(initialState.messages.length === 0 && submitState_1.messages.length === 1 && submitState_2.messages.length === 2 && submitState_2.messages[1] === testMessage_2, 'message: Clicking the Add message button should call the method submitMessage which should add the current input to the messages array in state.'); }; ",
+ "async () => { const mockedComponent = Enzyme.mount(React.createElement(DisplayMessages)); const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); const causeChange = (c, v) => c.find('input').simulate('change', { target: { value: v }}); const initialState = mockedComponent.state(); const testMessage = '__FIRST__MESSAGE__'; const firstChange = () => { causeChange(mockedComponent, testMessage); return waitForIt(() => mockedComponent )}; const firstResult = await firstChange(); const firstState = firstResult.state(); const firstSubmit = () => { mockedComponent.find('button').simulate('click'); return waitForIt(() => mockedComponent )}; const afterSubmit = await firstSubmit(); const submitState = afterSubmit.state(); assert(firstState.input === testMessage && submitState.input === '', 'message: The submitMessage method should clear the current input.'); }; "
+ ],
+ "solutions": [
+ "class DisplayMessages extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n input: '',\n messages: []\n }\n this.handleChange = this.handleChange.bind(this); \n this.submitMessage = this.submitMessage.bind(this); \n }\n handleChange(event) {\n this.setState({\n input: event.target.value\n });\n }\n submitMessage() {\n const currentMessage = this.state.input;\n this.setState({\n input: '',\n messages: this.state.messages.concat(currentMessage)\n });\n }\n render() {\n return (\n state into Redux. This is the first step to connect the simple React app to Redux. The only functionality your app has is to add new messages from the user to an unordered list. The example is simple in order to demonstrate how React and Redux work together.",
+ "ADD. Next, define an action creator addMessage() which creates the action to add a message. You'll need to pass a message to this action creator and include the message in the returned action.",
+ "Then create a reducer called messageReducer() that handles the state for the messages. The initial state should equal an empty array. This reducer should add a message to the array of messages held in state, or return the current state. Finally, create your Redux store and pass it the reducer."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "// define ADD, addMessage(), messageReducer(), and store here:",
+ ""
+ ]
+ }
+ },
+ "tests": [
+ "assert(ADD === 'ADD', 'message: The const ADD should exist and hold a value equal to the string ADD');",
+ "assert((function() { const addAction = addMessage('__TEST__MESSAGE__'); return addAction.type === ADD && addAction.message === '__TEST__MESSAGE__'; })(), 'message: The action creator addMessage should return an object with type equal to ADD and message equal to the message that is passed in.');",
+ "assert(typeof messageReducer === 'function', 'message: messageReducer should be a function.');",
+ "assert((function() { const initialState = store.getState(); return typeof store === 'object' && initialState.length === 0; })(), 'message: The store should exist and have an initial state set to an empty array.');",
+ "assert((function() { const initialState = store.getState(); const isFrozen = DeepFreeze(initialState); store.dispatch(addMessage('__A__TEST__MESSAGE')); const addState = store.getState(); return (isFrozen && addState[0] === '__A__TEST__MESSAGE'); })(), 'message: Dispatching addMessage against the store should immutably add a new message to the array of messages held in state.');",
+ "assert((function() { const addState = store.getState(); store.dispatch({type: 'FAKE_ACTION'}); const testState = store.getState(); return (addState === testState); })(), 'message: The messageReducer should return the current state if called with any other actions.');"
+ ],
+ "solutions": [
+ "const ADD = 'ADD';\n\nconst addMessage = (message) => {\n return {\n type: ADD,\n message\n }\n};\n\nconst messageReducer = (state = [], action) => {\n switch (action.type) {\n case ADD:\n return [\n ...state,\n action.message\n ];\n default:\n return state;\n }\n};\n\nconst store = Redux.createStore(messageReducer);"
+ ],
+ "type": "modern",
+ "isRequired": false,
+ "translations": {},
+ "reactRedux": true
+ },
+ {
+ "id": "5a24c314108439a4d4036144",
+ "title": "Use Provider to Connect Redux to React",
+ "releasedOn": "December 25, 2017",
+ "description": [
+ "In the last challenge, you created a Redux store to handle the messages array and created an action for adding new messages. The next step is to provide React access to the Redux store and the actions it needs to dispatch updates. React Redux provides its react-redux package to help accomplish these tasks.",
+ "React Redux provides a small API with two key features: Provider and connect. Another challenge covers connect. The Provider is a wrapper component from React Redux that wraps your React app. This wrapper then allows you to access the Redux store and dispatch functions throughout your component tree. Provider takes two props, the Redux store and the child components of your app. Defining the Provider for an App component might look like this:",
+ "<Provider store={store}>", + "
<App/>
</Provider>
DisplayMessages component. The only new piece is the AppWrapper component at the bottom. Use this top level component to render the Provider from ReactRedux, and pass the Redux store as a prop. Then render the DisplayMessages component as a child. Once you are finished, you should see your React component rendered to the page.",
+ "Note: React Redux is available as a global variable here, so you can access the Provider with dot notation. The code in the editor takes advantage of this and sets it to a constant Provider for you to use in the AppWrapper render method."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "// Redux Code:",
+ "const ADD = 'ADD';",
+ "",
+ "const addMessage = (message) => {",
+ " return {",
+ " type: ADD,",
+ " message",
+ " }",
+ "};",
+ "",
+ "const messageReducer = (state = [], action) => {",
+ " switch (action.type) {",
+ " case ADD:",
+ " return [",
+ " ...state,",
+ " action.message",
+ " ];",
+ " default:",
+ " return state;",
+ " }",
+ "};",
+ "",
+ "",
+ "",
+ "const store = Redux.createStore(messageReducer);",
+ "",
+ "// React Code:",
+ "",
+ "class DisplayMessages extends React.Component {",
+ " constructor(props) {",
+ " super(props);",
+ " this.state = {",
+ " input: '',",
+ " messages: []",
+ " }",
+ " this.handleChange = this.handleChange.bind(this);",
+ " this.submitMessage = this.submitMessage.bind(this);",
+ " }",
+ " handleChange(event) {",
+ " this.setState({",
+ " input: event.target.value",
+ " });",
+ " }",
+ " submitMessage() {",
+ " const currentMessage = this.state.input;",
+ " this.setState({",
+ " input: '',",
+ " messages: this.state.messages.concat(currentMessage)",
+ " });",
+ " }",
+ " render() {",
+ " return (",
+ " AppWrapper should render.');",
+ "getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return getUserInput('index').replace(/ /g,'').includes('Provider wrapper component should have a prop of store passed to it, equal to the Redux store.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return mockedComponent.find('AppWrapper').find('DisplayMessages').length === 1; })(), 'message: DisplayMessages should render as a child of AppWrapper.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return mockedComponent.find('div').length === 1 && mockedComponent.find('h2').length === 1 && mockedComponent.find('button').length === 1 && mockedComponent.find('ul').length === 1; })(), 'message: The DisplayMessages component should render an h2, input, button, and ul element.');"
+ ],
+ "solutions": [
+ "// Redux Code:\nconst ADD = 'ADD';\n\nconst addMessage = (message) => {\n return {\n type: ADD,\n message\n }\n};\n\nconst messageReducer = (state = [], action) => {\n switch (action.type) {\n case ADD:\n return [\n ...state,\n action.message\n ];\n default:\n return state;\n }\n};\n\nconst store = Redux.createStore(messageReducer);\n\n// React Code:\n\nclass DisplayMessages extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n input: '',\n messages: []\n }\n this.handleChange = this.handleChange.bind(this); \n this.submitMessage = this.submitMessage.bind(this); \n }\n handleChange(event) {\n this.setState({\n input: event.target.value\n });\n }\n submitMessage() {\n const currentMessage = this.state.input;\n this.setState({\n input: '',\n messages: this.state.messages.concat(currentMessage)\n });\n }\n render() {\n return (\n Provider component allows you to provide state and dispatch to your React components, but you must specify exactly what state and actions you want. This way, you make sure that each component only has access to the state it needs. You accomplish this by creating two functions: mapStateToProps() and mapDispatchToProps().",
+ "In these functions, you declare what pieces of state you want to have access to and which action creators you need to be able to dispatch. Once these functions are in place, you'll see how to use the React Redux connect method to connect them to your components in another challenge.",
+ "Note: Behind the scenes, React Redux uses the store.subscribe() method to implement mapStateToProps().",
+ "mapStateToProps(). This function should take state as an argument, then return an object which maps that state to specific property names. These properties will become accessible to your component via props. Since this example keeps the entire state of the app in a single array, you can pass that entire state to your component. Create a property messages in the object that's being returned, and set it to state."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const state = [];",
+ "",
+ "// change code below this line",
+ ""
+ ]
+ }
+ },
+ "tests": [
+ "assert(Array.isArray(state) && state.length === 0, 'message: The const state should be an empty array.');",
+ "assert(typeof mapStateToProps === 'function', 'message: mapStateToProps should be a function.');",
+ "assert(typeof mapStateToProps() === 'object', 'message: mapStateToProps should return an object.');",
+ "assert(mapStateToProps(['messages']).messages.pop() === 'messages', 'message: Passing an array as state to mapStateToProps should return this array assigned to a key of messages.');"
+ ],
+ "solutions": [
+ "const state = [];\n\n// change code below this line\n\nconst mapStateToProps = (state) => {\n return {\n messages: state\n }\n};"
+ ],
+ "type": "modern",
+ "isRequired": false,
+ "translations": {},
+ "reactRedux": true
+ },
+ {
+ "id": "5a24c314108439a4d4036146",
+ "title": "Map Dispatch to Props",
+ "releasedOn": "December 25, 2017",
+ "description": [
+ "The mapDispatchToProps() function is used to provide specific action creators to your React components so they can dispatch actions against the Redux store. It's similar in structure to the mapStateToProps() function you wrote in the last challenge. It returns an object that maps dispatch actions to property names, which become component props. However, instead of returning a piece of state, each property returns a function that calls dispatch with an action creator and any relevant action data. You have access to this dispatch because it's passed in to mapDispatchToProps() as a parameter when you define the function, just like you passed state to mapStateToProps(). Behind the scenes, React Redux is using Redux's store.dispatch() to conduct these dispatches with mapDispatchToProps(). This is similar to how it uses store.subscribe() for components that are mapped to state.",
+ "For example, you have a loginUser() action creator that takes a username as an action payload. The object returned from mapDispatchToProps() for this action creator would look something like:",
+ "{", + "
submitLoginUser: function(username) {
dispatch(loginUser(username));
}
}
addMessage(). Write the function mapDispatchToProps() that takes dispatch as an argument, then returns an object. The object should have a property submitNewMessage set to the dispatch function, which takes a parameter for the new message to add when it dispatches addMessage()."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const addMessage = (message) => {",
+ " return {",
+ " type: 'ADD',",
+ " message: message",
+ " }",
+ "};",
+ "",
+ "// change code below this line",
+ ""
+ ]
+ }
+ },
+ "tests": [
+ "assert((function() { const addMessageTest = addMessage(); return ( addMessageTest.hasOwnProperty('type') && addMessageTest.hasOwnProperty('message')); })(), 'message: addMessage should return an object with keys type and message.');",
+ "assert(typeof mapDispatchToProps === 'function', 'message: mapDispatchToProps should be a function.');",
+ "assert(typeof mapDispatchToProps() === 'object', 'message: mapDispatchToProps should return an object.');",
+ "assert((function() { let testAction; const dispatch = (fn) => { testAction = fn; }; let dispatchFn = mapDispatchToProps(dispatch); dispatchFn.submitNewMessage('__TEST__MESSAGE__'); return (testAction.type === 'ADD' && testAction.message === '__TEST__MESSAGE__'); })(), 'message: Dispatching addMessage with submitNewMessage from mapDispatchToProps should return a message to the dispatch function.');"
+ ],
+ "solutions": [
+ "const addMessage = (message) => {\n return {\n type: 'ADD',\n message: message\n }\n};\n\n// change code below this line\n\nconst mapDispatchToProps = (dispatch) => {\n return {\n submitNewMessage: function(message) {\n dispatch(addMessage(message));\n }\n }\n};"
+ ],
+ "type": "modern",
+ "isRequired": false,
+ "translations": {},
+ "reactRedux": true
+ },
+ {
+ "id": "5a24c314108439a4d4036147",
+ "title": "Connect Redux to React",
+ "releasedOn": "December 25, 2017",
+ "description": [
+ "Now that you've written both the mapStateToProps() and the mapDispatchToProps() functions, you can use them to map state and dispatch to the props of one of your React components. The connect method from React Redux can handle this task. This method takes two optional arguments, mapStateToProps() and mapDispatchToProps(). They are optional because you may have a component that only needs access to state but doesn't need to dispatch any actions, or vice versa.",
+ "To use this method, pass in the functions as arguments, and immediately call the result with your component. This syntax is a little unusual and looks like:",
+ "connect(mapStateToProps, mapDispatchToProps)(MyComponent)",
+ "Note: If you want to omit one of the arguments to the connect method, you pass null in its place.",
+ "mapStateToProps() and mapDispatchToProps() functions and a new React component called Presentational. Connect this component to Redux with the connect method from the ReactRedux global object, and call it immediately on the Presentational component. Assign the result to a new const called ConnectedComponent that represents the connected component. That's it, now you're connected to Redux! Try changing either of connect's arguments to null and observe the test results."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const addMessage = (message) => {",
+ " return {",
+ " type: 'ADD',",
+ " message: message",
+ " }",
+ "};",
+ "",
+ "const mapStateToProps = (state) => {",
+ " return {",
+ " messages: state",
+ " }",
+ "};",
+ "",
+ "const mapDispatchToProps = (dispatch) => {",
+ " return {",
+ " submitNewMessage: (message) => {",
+ " dispatch(addMessage(message));",
+ " }",
+ " }",
+ "};",
+ "",
+ "class Presentational extends React.Component {",
+ " constructor(props) {",
+ " super(props);",
+ " }",
+ " render() {",
+ " return Presentational component should render.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const props = mockedComponent.find('Presentational').props(); return props.messages === '__INITIAL__STATE__'; })(), 'message: The Presentational component should receive a prop messages via connect.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const props = mockedComponent.find('Presentational').props(); return typeof props.submitNewMessage === 'function'; })(), 'message: The Presentational component should receive a prop submitNewMessage via connect.');"
+ ],
+ "solutions": [
+ "const addMessage = (message) => {\n return {\n type: 'ADD',\n message: message\n }\n};\n\nconst mapStateToProps = (state) => {\n return {\n messages: state\n }\n};\n\nconst mapDispatchToProps = (dispatch) => {\n return {\n submitNewMessage: (message) => {\n dispatch(addMessage(message));\n }\n }\n};\n\nclass Presentational extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return connect to connect React to Redux, you can apply what you've learned to your React component that handles messages.",
+ "In the last lesson, the component you connected to Redux was named Presentational, and this wasn't arbitrary. This term generally refers to React components that are not directly connected to Redux. They are simply responsible for the presentation of UI and do this as a function of the props they receive. By contrast, container components are connected to Redux. These are typically responsible for dispatching actions to the store and often pass store state to child components as props.",
+ "Presentational. Create a new component held in a constant called Container that uses connect to connect the Presentational component to Redux. Then, in the AppWrapper, render the React Redux Provider component. Pass Provider the Redux store as a prop and render Container as a child. Once everything is setup, you will see the messages app rendered to the page again."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "// Redux:",
+ "const ADD = 'ADD';",
+ "",
+ "const addMessage = (message) => {",
+ " return {",
+ " type: ADD,",
+ " message: message",
+ " }",
+ "};",
+ "",
+ "const messageReducer = (state = [], action) => {",
+ " switch (action.type) {",
+ " case ADD:",
+ " return [",
+ " ...state,",
+ " action.message",
+ " ];",
+ " default:",
+ " return state;",
+ " }",
+ "};",
+ "",
+ "const store = Redux.createStore(messageReducer);",
+ "",
+ "// React:",
+ "class Presentational extends React.Component {",
+ " constructor(props) {",
+ " super(props);",
+ " this.state = {",
+ " input: '',",
+ " messages: []",
+ " }",
+ " this.handleChange = this.handleChange.bind(this);",
+ " this.submitMessage = this.submitMessage.bind(this);",
+ " }",
+ " handleChange(event) {",
+ " this.setState({",
+ " input: event.target.value",
+ " });",
+ " }",
+ " submitMessage() {",
+ " const currentMessage = this.state.input;",
+ " this.setState({",
+ " input: '',",
+ " messages: this.state.messages.concat(currentMessage)",
+ " });",
+ " }",
+ " render() {",
+ " return (",
+ " AppWrapper should render to the page.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return mockedComponent.find('Presentational').length === 1; })(), 'message: The Presentational component should render an h2, input, button, and ul elements.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const PresentationalComponent = mockedComponent.find('Presentational'); return ( PresentationalComponent.find('div').length === 1 && PresentationalComponent.find('h2').length === 1 && PresentationalComponent.find('button').length === 1 && PresentationalComponent.find('ul').length === 1 ); })(), 'message: The Presentational component should render an h2, input, button, and ul elements.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const PresentationalComponent = mockedComponent.find('Presentational'); const props = PresentationalComponent.props(); return Array.isArray(props.messages); })(), 'message: The Presentational component should receive messages from the Redux store as a prop.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const PresentationalComponent = mockedComponent.find('Presentational'); const props = PresentationalComponent.props(); return typeof props.submitNewMessage === 'function'; })(), 'message: The Presentational component should receive the submitMessage action creator as a prop.');"
+ ],
+ "solutions": [
+ "// Redux:\nconst ADD = 'ADD';\n\nconst addMessage = (message) => {\n return {\n type: ADD,\n message: message\n }\n};\n\nconst messageReducer = (state = [], action) => {\n switch (action.type) {\n case ADD:\n return [\n ...state,\n action.message\n ];\n default:\n return state;\n }\n};\n\nconst store = Redux.createStore(messageReducer);\n\n// React:\nclass Presentational extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n input: '',\n messages: []\n }\n this.handleChange = this.handleChange.bind(this); \n this.submitMessage = this.submitMessage.bind(this); \n }\n handleChange(event) {\n this.setState({\n input: event.target.value\n });\n }\n submitMessage() {\n const currentMessage = this.state.input;\n this.setState({\n input: '',\n messages: this.state.messages.concat(currentMessage)\n });\n }\n render() {\n return (\n Presentational component and into Redux. Currently, you have Redux connected, but you are handling the state locally within the Presentational component.",
+ "Presentational component, first, remove the messages property in the local state. These messages will be managed by Redux. Next, modify the submitMessage() method so that it dispatches submitNewMessage() from this.props, and pass in the current message input from local state as an argument. Because you removed messages from local state, remove the messages property from the call to this.setState() here as well. Finally, modify the render() method so that it maps over the messages received from props rather than state.",
+ "Once these changes are made, the app will continue to function the same, except Redux manages the state. This example also illustrates how a component may have local state: your component still tracks user input locally in its own state. You can see how Redux provides a useful state management framework on top of React. You achieved the same result using only React's local state at first, and this is usually possible with simple apps. However, as your apps become larger and more complex, so does your state management, and this is the problem Redux solves."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "// Redux:",
+ "const ADD = 'ADD';",
+ "",
+ "const addMessage = (message) => {",
+ " return {",
+ " type: ADD,",
+ " message: message",
+ " }",
+ "};",
+ "",
+ "const messageReducer = (state = [], action) => {",
+ " switch (action.type) {",
+ " case ADD:",
+ " return [",
+ " ...state,",
+ " action.message",
+ " ];",
+ " default:",
+ " return state;",
+ " }",
+ "};",
+ "",
+ "const store = Redux.createStore(messageReducer);",
+ "",
+ "// React:",
+ "const Provider = ReactRedux.Provider;",
+ "const connect = ReactRedux.connect;",
+ "",
+ "// Change code below this line",
+ "class Presentational extends React.Component {",
+ " constructor(props) {",
+ " super(props);",
+ " this.state = {",
+ " input: '',",
+ " messages: []",
+ " }",
+ " this.handleChange = this.handleChange.bind(this);",
+ " this.submitMessage = this.submitMessage.bind(this);",
+ " }",
+ " handleChange(event) {",
+ " this.setState({",
+ " input: event.target.value",
+ " });",
+ " }",
+ " submitMessage() {",
+ " this.setState({",
+ " input: '',",
+ " messages: this.state.messages.concat(this.state.input)",
+ " });",
+ " }",
+ " render() {",
+ " return (",
+ " AppWrapper should render to the page.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); return mockedComponent.find('Presentational').length === 1; })(), 'message: The Presentational component should render an h2, input, button, and ul elements.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const PresentationalComponent = mockedComponent.find('Presentational'); return ( PresentationalComponent.find('div').length === 1 && PresentationalComponent.find('h2').length === 1 && PresentationalComponent.find('button').length === 1 && PresentationalComponent.find('ul').length === 1 ); })(), 'message: The Presentational component should render an h2, input, button, and ul elements.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const PresentationalComponent = mockedComponent.find('Presentational'); const props = PresentationalComponent.props(); return Array.isArray(props.messages); })(), 'message: The Presentational component should receive messages from the Redux store as a prop.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const PresentationalComponent = mockedComponent.find('Presentational'); const props = PresentationalComponent.props(); return typeof props.submitNewMessage === 'function'; })(), 'message: The Presentational component should receive the submitMessage action creator as a prop.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const PresentationalState = mockedComponent.find('Presentational').instance().state; return typeof PresentationalState.input === 'string' && Object.keys(PresentationalState).length === 1; })(), 'message: The state of the Presentational component should contain one property, input, which is initialized to an empty string.');",
+ "async () => { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const testValue = '__MOCK__INPUT__'; const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); const causeChange = (c, v) => c.find('input').simulate('change', { target: { value: v }}); let initialInput = mockedComponent.find('Presentational').find('input'); const changed = () => { causeChange(mockedComponent, testValue); return waitForIt(() => mockedComponent )}; const updated = await changed(); const updatedInput = updated.find('Presentational').find('input'); assert(initialInput.props().value === '' && updatedInput.props().value === '__MOCK__INPUT__', 'message: Typing in the input element should update the state of the Presentational component.'); }; ",
+ "async () => { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); let beforeProps = mockedComponent.find('Presentational').props(); const testValue = '__TEST__EVENT__INPUT__'; const causeChange = (c, v) => c.find('input').simulate('change', { target: { value: v }}); const changed = () => { causeChange(mockedComponent, testValue); return waitForIt(() => mockedComponent )}; const clickButton = () => { mockedComponent.find('button').simulate('click'); return waitForIt(() => mockedComponent )}; const afterChange = await changed(); const afterChangeInput = afterChange.find('input').props().value; const afterClick = await clickButton(); const afterProps = mockedComponent.find('Presentational').props(); assert(beforeProps.messages.length === 0 && afterChangeInput === testValue && afterProps.messages.pop() === testValue && afterClick.find('input').props().value === '', 'message: Dispatching the submitMessage on the Presentational component should update Redux store and clear the input in local state.'); }; ",
+ "async () => { const mockedComponent = Enzyme.mount(React.createElement(AppWrapper)); const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); let beforeProps = mockedComponent.find('Presentational').props(); const testValue = '__TEST__EVENT__INPUT__'; const causeChange = (c, v) => c.find('input').simulate('change', { target: { value: v }}); const changed = () => { causeChange(mockedComponent, testValue); return waitForIt(() => mockedComponent )}; const clickButton = () => { mockedComponent.find('button').simulate('click'); return waitForIt(() => mockedComponent )}; const afterChange = await changed(); const afterChangeInput = afterChange.find('input').props().value; const afterClick = await clickButton(); const afterProps = mockedComponent.find('Presentational').props(); assert(beforeProps.messages.length === 0 && afterChangeInput === testValue && afterProps.messages.pop() === testValue && afterClick.find('input').props().value === '' && afterClick.find('ul').childAt(0).text() === testValue, 'message: The Presentational component should render the messages from the Redux store.'); }; "
+ ],
+ "solutions": [
+ "// Redux:\nconst ADD = 'ADD';\n\nconst addMessage = (message) => {\n return {\n type: ADD,\n message: message\n }\n};\n\nconst messageReducer = (state = [], action) => {\n switch (action.type) {\n case ADD:\n return [\n ...state,\n action.message\n ];\n default:\n return state;\n }\n};\n\nconst store = Redux.createStore(messageReducer);\n\n// React:\nconst Provider = ReactRedux.Provider;\nconst connect = ReactRedux.connect;\n\n// Change code below this line\nclass Presentational extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n input: ''\n }\n this.handleChange = this.handleChange.bind(this); \n this.submitMessage = this.submitMessage.bind(this); \n }\n handleChange(event) {\n this.setState({\n input: event.target.value\n });\n }\n submitMessage() {\n this.props.submitNewMessage(this.state.input);\n this.setState({\n input: ''\n });\n }\n render() {\n return (\n import statements (these pull in all of the dependencies that have been provided for you in the challenges). The \"Managing Packages with npm\" section covers npm in more detail.",
+ "Finally, writing React and Redux code generally requires some configuration. This can get complicated quickly. If you are interested in experimenting on your own machine, the",
+ "Create React App comes configured and ready to go.",
+ "Alternatively, you can enable Babel as a JavaScript Preprocessor in CodePen, add React and ReactDOM as external JavaScript resources, and work there as well.",
+ "'Now I know React and Redux!' to the console."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "// import React from 'react'",
+ "// import ReactDOM from 'react-dom'",
+ "// import { Provider, connect } from 'react-redux'",
+ "// import { createStore, combineReducers, applyMiddleware } from 'redux'",
+ "// import thunk from 'redux-thunk'",
+ "",
+ "// import rootReducer from './redux/reducers'",
+ "// import App from './components/App'",
+ "",
+ "// const store = createStore(",
+ "// rootReducer,",
+ "// applyMiddleware(thunk)",
+ "// );",
+ "",
+ "// ReactDOM.render(",
+ "// Now I know React and Redux! should be logged to the console.');"
+ ],
+ "solutions": [
+ "console.log('Now I know React and Redux!');"
+ ],
+ "type": "modern",
+ "isRequired": false,
+ "translations": {},
+ "reactRedux": true
}
]
-}
\ No newline at end of file
+}
diff --git a/seed/challenges/03-front-end-libraries/react.json b/seed/challenges/03-front-end-libraries/react.json
index ee7019b9959..6ee2923436b 100644
--- a/seed/challenges/03-front-end-libraries/react.json
+++ b/seed/challenges/03-front-end-libraries/react.json
@@ -1,17 +1,23 @@
{
+
"name": "React",
"order": 5,
"time": "5 hours",
"helpRoom": "Help",
"required": [
{
- "src": "https://cdnjs.cloudflare.com/ajax/libs/react/16.1.1/umd/react.development.js"
+ "src": "https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.js"
+ },
+ {
+ "src": "https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react-dom.js"
}
],
+ "template": "${ source }",
"challenges": [
{
"id": "587d7dbc367417b2b2512bb1",
"title": "Create a Simple JSX Element",
+ "releasedOn": "December 25, 2017",
"description": [
"Intro: React is an Open Source view library created and maintained by Facebook. It's a great tool to render the User Interface (UI) of modern web applications.",
"React uses a syntax extension of JavaScript called JSX that allows you to write HTML directly within JavaScript. This has several benefits. It lets you use the full programmatic power of JavaScript within HTML, and helps to keep your code readable. For the most part, JSX is similar to the HTML that you have already learned, however there are a few key differences that will be covered throughout these challenges.",
@@ -27,18 +33,670 @@
"name": "index",
"contents": [
"",
- "const jsx = ;",
+ "const JSX = ;",
""
+ ],
+ "tail": "ReactDOM.render(JSX, document.getElementById('root'))"
+ }
+ },
+ "tests": [
+ "assert(JSX.type === 'h1', 'message: The constant JSX should return an h1 element.');",
+ "assert(Enzyme.shallow(JSX).contains('Hello JSX!'), 'message: The h1 tag should include the text Hello JSX!');"
+ ],
+ "solutions": [
+ "const JSX = <div>", + "Invalid JSX:", + "
<p>Paragraph One</p>
<p>Paragraph Two</p>
<p>Paragraph Three</p>
</div>
<p>Paragraph One</p>", + "
<p>Paragraph Two</p>
<p>Paragraph Three</p>
JSX that renders a div which contains the following elements in order:",
+ "An h1, a p, and an unordered list that contains three li items. You can include any text you want within each element.",
+ "Note: When rendering multiple elements like this, you can wrap them all in parentheses, but it's not strictly required. Also notice this challenge uses a div tag to wrap all the child elements within a single parent element. If you remove the div, the JSX will no longer transpile. Keep this in mind, since it will also apply when you return JSX elements in React components."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "// write your code here",
+ ""
+ ],
+ "tail": "ReactDOM.render(JSX, document.getElementById('root'))"
+ }
+ },
+ "tests": [
+ "assert(JSX.type === 'div', 'message: The constant JSX should return a div element.');",
+ "assert(JSX.props.children[1].type === 'p', 'message: The div should contain a p tag as the second element.');",
+ "assert(JSX.props.children[2].type === 'ul', 'message: The div should contain a ul tag as the third element.');",
+ "assert(JSX.props.children[0].type === 'h1', 'message: The div should contain an h1 tag as the first element.');",
+ "assert(JSX.props.children[2].props.children.length === 3, 'message: The ul should contain three li elements.');"
+ ],
+ "solutions": [
+ "const JSX = (\nSome info
\n{/* */} to wrap around the comment text.",
+ "div element, without modifying the existing h1 or p elements."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const JSX = (",
+ " Here's a subtitle
", + "JSX should return a div element.');",
+ "assert(JSX.props.children[0].type === 'h1', 'message: The div should contain an h1 tag as the first element.');",
+ "assert(JSX.props.children[1].type === 'p', 'message: The div should contain a p tag as the second element.');",
+ "getUserInput => assert(getUserInput('index').includes('/*') && getUserInput('index').includes('*/'), 'message: The JSX should include a comment.');"
+ ],
+ "solutions": [
+ "const JSX = (\nHere's a subtitle
\nReactDOM.render(componentToRender, targetNode).",
+ "ReactDOM.render() must be called after the JSX element declarations, just like how you must declare variables before using them.",
+ "ReactDOM.render() method to render this component to the page. You can pass defined JSX elements directly in as the first argument and use document.getElementById() to select the DOM node to render them to. There is a div with id='challenge-node' available for you to use. Make sure you don't change the JSX constant."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const JSX = (",
+ " Lets render this to the DOM
", + "JSX should return a div element.');",
+ "assert(JSX.props.children[0].type === 'h1', 'message: The div should contain an h1 tag as the first element.');",
+ "assert(JSX.props.children[1].type === 'p', 'message: The div should contain a p tag as the second element.');",
+ "assert(document.getElementById('challenge-node').childNodes[0].innerHTML === 'Lets render this to the DOM
', 'message: The provided JSX element should render to the DOM node with idchallenge-node.');"
+ ],
+ "solutions": [
+ "const JSX = (\nLets render this to the DOM
\nclass to define HTML classes. This is because class is a reserved word in JavaScript. Instead, JSX uses className.",
+ "In fact, the naming convention for all HTML attributes and event references in JSX become camelCase. For example, a click event in JSX is onClick, instead of onclick. Likewise, onchange becomes onChange. While this is a subtle difference, it is an important one to keep in mind moving forward.",
+ "myDiv to the div provided in the JSX code."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const JSX = (",
+ " JSX should return a div element.');",
+ "assert.strictEqual(JSX.props.className, 'myDiv', 'message: The div has a class of myDiv.');"
+ ],
+ "solutions": [
+ "const JSX = (\nclassName vs. class for defining HTML classes.",
+ "Another important way in which JSX differs from HTML is in the idea of the self-closing tag.",
+ "In HTML, almost all tags have both an opening and closing tag: <div></div>; the closing tag always has a forward slash before the tag name that you are closing. However, there are special instances in HTML called “self-closing tags”, or tags that don’t require both an opening and closing tag before another tag can start.",
+ "For example the line-break tag can be written as <br> or as <br />, but should never be written as <br></br>, since it doesn't contain any content.",
+ "In JSX, the rules are a little different. Any JSX element can be written with a self-closing tag, and every element must be closed. The line-break tag, for example, must always be written as <br /> in order to be valid JSX that can be transpiled. A <div>, on the other hand, can be written as <div /> or <div></div>. The difference is that in the first syntax version there is no way to include anything in the <div />. You will see in later challenges that this syntax is useful when rendering React components.",
+ "Be sure to close all tags!
", + "h1 element.');",
- "assert(Enzyme.shallow(jsx).contains( 'Hello JSX!'), 'message: The h1 tag should include the text Hello JSX!');"
+ "assert.strictEqual(JSX.type, 'div', 'message: The constant JSX should return a div element.');",
+ "assert(Enzyme.shallow(JSX).find('br').length === 1, 'message: The div should contain a br tag.');",
+ "assert(Enzyme.shallow(JSX).find('hr').length === 1, 'message: The div should contain an hr tag.');",
+ "assert((() => { const testDiv = document.getElementById('challenge-node').childNodes[0].innerHTML.replace(/\\s/g,''); return testDiv.includes('Besuretoclosealltags!
'); })(), 'message: The provided JSX element should render as is to the DOM node withid of challenge-node.');"
+ ],
+ "solutions": [
+ "const JSX = (\nBe sure to close all tags!
\nnull. One important thing to note is that React requires your function name to begin with a capital letter. Here's an example of a stateless functional component that assigns an HTML class in JSX:",
+ "// After being transpiled, the <div> will have a CSS class of 'customClass'", + "Because a JSX component represents HTML, you could put several components together to create a more complex HTML page. This is one of the key advantages of the component architecture React provides. It allows you to compose your UI from many separate, isolated components. This makes it easier to build and maintain complex user interfaces.", + "
const DemoComponent = function() {
return (
<div className='customClass' />
);
};
MyComponent. Complete this function so it returns a single div element which contains some string of text.",
+ "Note: The text is considered a child of the div element, so you will not be able to use a self-closing tag."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const MyComponent = function() {",
+ " // change code below this line",
+ "",
+ "",
+ "",
+ " // change code above this line",
+ "}"
+ ],
+ "tail": "ReactDOM.render(MyComponent should return JSX.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.children().type() === 'div' })(), 'message: MyComponent should return a div element.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.find('div').text() !== ''; })(), 'message: The div element should contain a string of text.');"
+ ],
+ "solutions": [
+ "\nconst MyComponent = function() {\n // change code below this line\n return (\n class syntax. In the following example, Kitten extends React.Component:",
+ "class Kitten extends React.Component {", + "This creates an ES6 class
constructor(props) {
super(props);
}
render() {
return (
<h1>Hi</h1>
);
}
}
Kitten which extends the React.Component class. So the Kitten class now has access to many useful React features, such as local state and lifecycle hooks. Don't worry if you aren't familiar with these terms yet, they will be covered in greater detail in later challenges.",
+ "Also notice the Kitten class has a constructor defined within it that calls super(). It uses super() to call the constructor of the parent class, in this case React.Component. The constructor is a special method used during the initialization of objects that are created with the class keyword. It is best practice to call a component's constructor with super, and pass props to both. This makes sure the component is initialized properly. For now, know that it is standard for this code to be included. Soon you will see other uses for the constructor as well as props.",
+ "MyComponent is defined in the code editor using class syntax. Finish writing the render method so it returns a div element that contains an h1 with the text Hello React!."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "",
+ "class MyComponent extends React.Component {",
+ " constructor(props) {",
+ " super(props);",
+ " }",
+ " render() {",
+ " // change code below this line",
+ "",
+ "",
+ "",
+ " // change code above this line",
+ " }",
+ "};"
+ ],
+ "tail": "ReactDOM.render(div element.');",
+ "assert(/div should render an h1 header within it.');",
+ "assert(Enzyme.shallow(React.createElement(MyComponent)).html() === 'h1 header should contain the string Hello React!.');"
+ ],
+ "solutions": [
+ "\nclass MyComponent extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n // change code below this line\n return (\n Navbar, Dashboard, and Footer.",
+ "To compose these components together, you could create an App parent component which renders each of these three components as children. To render a component as a child in a React component, you include the component name written as a custom HTML tag in the JSX. For example, in the render method you could write:",
+ "return (", + "When React encounters a custom HTML tag that references another component (a component name wrapped in
<App>
<Navbar />
<Dashboard />
<Footer />
</App>
)
< /> like in this example), it renders the markup for that component in the location of the tag. This should illustrate the parent/child relationship between the App component and the Navbar, Dashboard, and Footer.",
+ "ChildComponent and a React component called ParentComponent. Compose the two together by rendering the ChildComponent within the ParentComponent. Make sure to close the ChildComponent tag with a forward slash.",
+ "Note: ChildComponent is defined with an ES6 arrow function because this is a very common practice when using React. However, know that this is just a function. If you aren't familiar with the arrow function syntax, please refer to the JavaScript section."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const ChildComponent = () => {",
+ " return (",
+ " I am the child
", + "div element.');",
+ "assert((function() { var shallowRender = Enzyme.shallow(React.createElement(ParentComponent)); return shallowRender.children().length === 2; })(), 'message: The component should return two nested elements.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ParentComponent)); return mockedComponent.find('ParentComponent').find('ChildComponent').length === 1; })(), 'message: The component should return the ChildComponent as its second child.');"
+ ],
+ "solutions": [
+ "const ChildComponent = () => {\n return (\n I am the child
\nTypesOfFruit and Fruits. Take the TypesOfFruit component and compose it, or nest it, within the Fruits component. Then take the Fruits component and nest it within the TypesOfFood component. The result should be a child component, nested within a parent component, which is nested within a parent component of its own!"
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const TypesOfFruit = () => {",
+ " return (",
+ " TypesOfFood component should return a single div element.');",
+ "assert(Enzyme.shallow(React.createElement(TypesOfFood)).props().children[1].type.name === 'Fruits', 'message: The TypesOfFood component should return the Fruits component.');",
+ "assert(Enzyme.mount(React.createElement(TypesOfFood)).find('h2').html() === 'Fruits component should return the TypesOfFruit component.');",
+ "assert(Enzyme.mount(React.createElement(TypesOfFood)).find('ul').text() === 'ApplesBlueberriesStrawberriesBananas', 'message: The TypesOfFruit component should return the h2 and ul elements.');"
+ ],
+ "solutions": [
+ "const TypesOfFruit = () => {\n return (\n TypesOfFood component is already rendering a component called Vegetables. Also, there is the Fruits component from the last challenge.",
+ "Nest two components inside of Fruits — first NonCitrus, and then Citrus. Both of these components are provided for you in the background. Next, nest the Fruits class component into the the TypesOfFood component, below the h1 header and above Vegetables. The result should be a series of nested components, which uses two different component types."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "head": [
+ "class NonCitrus extends React.Component {",
+ " render() {",
+ " return (",
+ " TypesOfFood component should return a single div element.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(TypesOfFood)); return mockedComponent.children().childAt(1).name() === 'Fruits'; })(), 'message: The TypesOfFood component should return the Fruits component.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(TypesOfFood)); return (mockedComponent.find('Fruits').children().find('NonCitrus') && mockedComponent.find('Fruits').children().find('Citrus')); })(), 'message: The Fruits component should return the NonCitrus component and the Citrus component.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(TypesOfFood)); return mockedComponent.children().childAt(2).name() === 'Vegetables'; })(), 'message: The TypesOfFood component should return the Vegetables component below the Fruits component.');"
+ ],
+ "solutions": [
+ "class Fruits extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\n ReactDOM.render(componentToRender, targetNode). The first argument is the React component that you want to render. The second argument is the DOM node that you want to render that component within.",
+ "React components are passed into ReactDOM.render() a little differently than JSX elements. For JSX elements, you pass in the name of the element that you want to render. However, for React components, you need to use the same syntax as if you were rendering a nested component, for example ReactDOM.render(<ComponentToRender />, targetNode). You use this syntax for both ES6 class components and functional components.",
+ "Fruits and Vegetables components are defined for you behind the scenes. Render both components as children of the TypesOfFood component, then render TypesOfFood to the DOM. There is a div with id='challenge-node' available for you to use."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "head": [
+ "",
+ "const Fruits = () => {",
+ " return (",
+ " TypesOfFood component should return a single div element.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(TypesOfFood)); return mockedComponent.children().childAt(1).name() === 'Fruits'; })(), 'message: The TypesOfFood component should render the Fruits component after the h1 element.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(TypesOfFood)); return mockedComponent.children().childAt(2).name() === 'Vegetables'; })(), 'message: The TypesOfFood component should render the Vegetables component after Fruits.');",
+ "assert((function() { const html = document.getElementById('challenge-node').childNodes[0].innerHTML; return (html === 'TypesOfFood component should render to the DOM within the div with the id challenge-node.');"
+ ],
+ "solutions": [
+ "\nclass TypesOfFood extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\n MyComponent.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.find('h1').text() === 'My First React Component!'; })(), 'message: MyComponent should contain an h1 tag with text My First React Component! Case and punctuation matter.');",
+ "assert(document.getElementById('challenge-node').childNodes.length === 1, 'message: MyComponent should render to the DOM.');"
+ ],
+ "solutions": [
+ "// change code below this line\nclass MyComponent extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\n App component which renders a child component called Welcome that is a stateless functional component. You can pass Welcome a user property by writing:",
+ "<App>", + "You use custom HTML attributes that React provides support for to pass the property
<Welcome user='Mark' />
</App>
user to the component Welcome. Since Welcome is a stateless functional component, it has access to this value like so:",
+ "const Welcome = (props) => <h1>Hello, {props.user}!</h1>", + "It is standard to call this value
props and when dealing with stateless functional components, you basically consider it as an argument to a function which returns JSX. You can access the value of the argument in the function body. With class components, you will see this is a little different.",
+ "Calendar and CurrentDate component in the code editor. When rendering CurrentDate from the Calendar component, pass in a property of date assigned to the current date from JavaScript's Date object. Then access this prop in the CurrentDate component, showing its value within the p tags. Note that for prop values to be evaluated as JavaScript, they must be enclosed in curly brackets, for instance date={Date()}."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "",
+ "const CurrentDate = (props) => {",
+ " return (",
+ " The current date is:
", + " { /* change code above this line */ }", + "Calendar component should return a single div element.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(Calendar)); return mockedComponent.children().childAt(1).name() === 'CurrentDate'; })(), 'message: The second child of the Calendar component should be the CurrentDate component.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(Calendar)); return mockedComponent.children().childAt(1).props().date })(), 'message: The CurrentDate component should have a prop called date.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(Calendar)); const prop = mockedComponent.children().childAt(1).props().date; return( typeof prop === 'string' && prop.length > 0 ); })(), 'message: The date prop of the CurrentDate should contain a string of text.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(Calendar)); return mockedComponent.find('p').html().includes(Date().substr(3)); })(), 'message: The CurrentDate component should render the value from the date prop in the p tag.');"
+ ],
+ "solutions": [
+ "\nconst CurrentDate = (props) => {\n return (\n The current date is: {props.date}
\n { /* change code above this line */ }\nprops or properties. This challenge looks at how arrays can be passed as props. To pass an array to a JSX element, it must be treated as JavaScript and wrapped in curly braces.",
+ "<ParentComponent>", + "The child component then has access to the array property
<ChildComponent colors={[\"green\", \"blue\", \"red\"]} />
</ParentComponent>
colors. Array methods such as join() can be used when accessing the property.",
+ "const ChildComponent = (props) => <p>{props.colors.join(', ')}</p>",
+ "This will join all colors array items into a comma separated string and produce:",
+ " <p>green, blue, red</p>",
+ "Later, we will learn about other common methods to render arrays of data in React.",
+ "List and ToDo components in the code editor. When rendering each List from the ToDo component, pass in a tasks property assigned to an array of to-do tasks, for example [\"walk dog\", \"workout\"]. Then access this tasks array in the List component, showing its value within the p element. Use join(\", \") to display the props.tasksarray in the p element as a comma separated list. Today's list should have at least 2 tasks and tomorrow's should have at least 3 tasks."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const List= (props) => {",
+ " { /* change code below this line */ }",
+ " return {}
", + " { /* change code above this line */ }", + "};", + "", + "class ToDo extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " render() {", + " return (", + "ToDo component should return a single outer div.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ToDo)); return mockedComponent.children().first().childAt(2).name() === 'List'; })(), 'message: The third child of the ToDo component should be an instance of the List component.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ToDo)); return mockedComponent.children().first().childAt(4).name() === 'List'; })(), 'message: The fifth child of the ToDo component should be an instance of the List component.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ToDo)); return Array.isArray(mockedComponent.find('List').get(0).props.tasks) && Array.isArray(mockedComponent.find('List').get(1).props.tasks); })(), 'message: Both instances of the List component should have a property called tasks and tasks should be of type array.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ToDo)); return mockedComponent.find('List').get(0).props.tasks.length >= 2; })(), 'message: The first List component representing the tasks for today should have 2 or more items.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ToDo)); return mockedComponent.find('List').get(1).props.tasks.length >= 3; })(), 'message: The second List component representing the tasks for tomorrow should have 3 or more items.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ToDo)); return mockedComponent.find('p').get(0).props.children === mockedComponent.find('List').get(0).props.tasks.join(', ') && mockedComponent.find('p').get(1).props.children === mockedComponent.find('List').get(1).props.tasks.join(', '); })(), 'message: The List component should render the value from the tasks prop in the p tag.');"
+ ],
+ "solutions": [
+ "const List= (props) => {\n return {props.tasks.join(', ')}
\n};\n\nclass ToDo extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\nMyComponent.defaultProps = { location: 'San Francisco' }, you have defined a location prop that's set to the string San Francisco, unless you specify otherwise. React assigns default props if props are undefined, but if you pass null as the value for a prop, it will remain null.",
+ "ShoppingCart component. Define default props on this component which specify a prop items with a value of 0."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const ShoppingCart = (props) => {",
+ " return (",
+ " ShoppingCart component should render.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); mockedComponent.setProps({items: undefined}); return mockedComponent.find('ShoppingCart').props().items === 0; })(), 'message: The ShoppingCart component should have a default prop of { items: 0 }.');"
+ ],
+ "solutions": [
+ "const ShoppingCart = (props) => {\n return (\n ShoppingCart component now renders a child component Items. This Items component has a default prop quantity set to the integer 0. Override the default prop by passing in a value of 10 for quantity.",
+ "Note: Remember that the syntax to add a prop to a component looks similar to how you add HTML attributes. However, since the value for quantity is an integer, it won't go in quotes but it should be wrapped in curly braces. For example, {100}. This syntax tells JSX to interpret the value within the braces directly as JavaScript."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "const Items = (props) => {",
+ " return ShoppingCart should render.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').length === 1; })(), 'message: The component Items should render.');",
+ "getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').props().quantity == 10 && getUserInput('index').replace(/ /g,'').includes('Items component should have a prop of { quantity: 10 } passed from the ShoppingCart component.');"
+ ],
+ "solutions": [
+ "const Items = (props) => {\n return propTypes on your component to require the data to be of type array. This will throw a useful warning when the data is of any other type.",
+ "It's considered a best practice to set propTypes when you know the type of a prop ahead of time. You can define a propTypes property for a component in the same way you defined defaultProps. Doing this will check that props of a given key are present with a given type. Here's an example to require the type function for a prop called handleClick:",
+ "MyComponent.propTypes = { handleClick: PropTypes.func.isRequired }",
+ "In the example above, the PropTypes.func part checks that handleClick is a function. Adding isRequired tells React that handleClick is a required property for that component. You will see a warning if that prop isn't provided. Also notice that func represents function. Among the seven JavaScript primitive types, function and boolean (written as bool) are the only two that use unusual spelling. In addition to the primitive types, there are other types available. For example, you can check that a prop is a React element. Please refer to the documentation for all of the options.",
+ "Note: As of React v15.5.0, PropTypes is imported independently from React, like this:",
+ "import React, { PropTypes } from 'react';",
+ "propTypes for the Items component to require quantity as a prop and verify that it is of type number."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "head": [
+ "var PropTypes = {",
+ " number: { isRequired: true }",
+ "};",
+ ""
+ ],
+ "contents": [
+ "const Items = (props) => {",
+ " return ShoppingCart component should render.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ShoppingCart)); return mockedComponent.find('Items').length === 1; })(), 'message: The Items component should render.');",
+ "getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/ /g, ''); return noWhiteSpace.includes('quantity:PropTypes.number.isRequired') && noWhiteSpace.includes('Items.propTypes='); })(), 'message: The Items component should include a propTypes check that requires quantity to be a number.');"
+ ],
+ "solutions": [
+ "const Items = (props) => {\n return this keyword. To access props within a class component, you preface the code that you use to access it with this. For example, if an ES6 class component has a prop called data, you write {this.props.data} in JSX.",
+ "ReturnTempPassword component in the parent component ResetPassword. Here, give ReturnTempPassword a prop of tempPassword and assign it a value of a string that is at least 8 characters long. Within the child, ReturnTempPassword, access the tempPassword prop within the strong tags to make sure the user sees the temporary password."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "contents": [
+ "class ReturnTempPassword extends React.Component {",
+ " constructor(props) {",
+ " super(props);",
+ "",
+ " }",
+ " render() {",
+ " return (",
+ " Your temporary password is:
", + " { /* change code above this line */ }", + "ResetPassword component should return a single div element.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ResetPassword)); return mockedComponent.children().childAt(3).name() === 'ReturnTempPassword'; })(), 'message: The fourth child of ResetPassword should be the ReturnTempPassword component.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ResetPassword)); return mockedComponent.find('ReturnTempPassword').props().tempPassword; })(), 'message: The ReturnTempPassword component should have a prop called tempPassword.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ResetPassword)); const temp = mockedComponent.find('ReturnTempPassword').props().tempPassword; return typeof temp === 'string' && temp.length >= 8; })(), 'message: The tempPassword prop of ReturnTempPassword should be equal to a string of at least 8 characters.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(ResetPassword)); return mockedComponent.find('strong').text() && mockedComponent.find('ReturnTempPassword').props().tempPassword; })(), 'message: The ReturnTempPassword component should display the password you create as the tempPassword prop within strong tags.');"
+ ],
+ "solutions": [
+ "class ReturnTempPassword extends React.Component {\n constructor(props) {\n super(props);\n\n }\n render() {\n return (\n Your temporary password is: {this.props.tempPassword}
\nReact.Component, but does not use internal state (covered in the next challenge). Finally, a stateful component is any component that does maintain its own internal state. You may see stateful components referred to simply as components or React components.",
+ "A common pattern is to try to minimize statefulness and to create stateless functional components wherever possible. This helps contain your state management to a specific area of your application. In turn, this improves development and maintenance of your app by making it easier to follow how changes to state affect its behavior.",
+ "CampSite component that renders a Camper component as a child. Define the Camper component and assign it default props of { name: 'CamperBot' }. Inside the Camper component, render any code that you want, but make sure to have one p element that includes only the name value that is passed in as a prop. Finally, define propTypes on the Camper component to require name to be provided as a prop and verify that it is of type string."
+ ],
+ "files": {
+ "indexjsx": {
+ "key": "indexjsx",
+ "ext": "jsx",
+ "name": "index",
+ "head": [
+ "var PropTypes = {",
+ " string: { isRequired: true }",
+ "};"
+ ],
+ "contents": [
+ "class CampSite extends React.Component {",
+ " constructor(props) {",
+ " super(props);",
+ " }",
+ " render() {",
+ " return (",
+ " CampSite component should render.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); return mockedComponent.find('Camper').length === 1; })(), 'message: The Camper component should render.');",
+ "getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/ /g, '').replace(/\\r?\\n|\\r/g, ''); const verify1 = 'Camper.defaultProps={name:\\'CamperBot\\'}'; const verify2 = 'Camper.defaultProps={name:\"CamperBot\"}'; return (noWhiteSpace.includes(verify1) || noWhiteSpace.includes(verify2)); })(), 'message: The Camper component should include default props which assign the string CamperBot to the key name.');",
+ "getUserInput => assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); const noWhiteSpace = getUserInput('index').replace(/ /g, '').replace(/\\r?\\n|\\r/g, ''); const verifyDefaultProps = 'Camper.propTypes={name:PropTypes.string.isRequired'; return noWhiteSpace.includes(verifyDefaultProps); })(), 'message: The Camper component should include prop types which require the name prop to be of type string.');",
+ "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(CampSite)); return mockedComponent.find('p').text() === mockedComponent.find('Camper').props().name; })(), 'message: The Camper component should contain a p element with only the text from the name prop.');"
+ ],
+ "solutions": [
+ "class CampSite extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\n {props.name}
\nstate. State consists of any data your application needs to know about, that can change over time. You want your apps to respond to state changes and present an updated UI when necessary. React offers a nice solution for the state management of modern web applications.",
+ "You create state in a React component by declaring a state property on the component class in its constructor. This initializes the component with state when it is created. The state property must be set to a JavaScript object. Declaring it looks like this:",
+ "this.state = {
// describe your state here
}", + "You have access to thestateobject throughout the life of your component. You can update it, render it in your UI, and pass it as props to child components. Thestateobject can be as complex or as simple as you need it to be. Note that you must create a class component by extendingReact.Componentin order to createstatelike this.", + "
", + "There is a component in the code editor that is trying to render anameproperty from itsstate. However, there is nostatedefined. Initialize the component withstatein theconstructorand assign your name to a property ofname." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "", + "class StatefulComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " // initialize state here", + "", + " }", + " render() {", + " return (", + "", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render({this.state.name}
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(StatefulComponent)); return mockedComponent.find('StatefulComponent').length === 1; })(), 'message: StatefulComponentshould exist and render.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(StatefulComponent)); return mockedComponent.find('div').length === 1 && mockedComponent.find('h1').length === 1; })(), 'message:StatefulComponentshould render adivand anh1element.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(StatefulComponent)); const initialState = mockedComponent.state(); return ( typeof initialState === 'object' && typeof initialState.name === 'string'); })(), 'message: The state ofStatefulComponentshould be initalized with a propertynameset to a string.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(StatefulComponent)); const initialState = mockedComponent.state(); return mockedComponent.find('h1').text() === initialState.name; })(), 'message: The propertynamein the state ofStatefulComponentshould render in theh1element.');" + ], + "solutions": [ + "\nclass StatefulComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n name: 'freeCodeCamp!'\n }\n }\n render() {\n return (\n\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036171", + "title": "Render State in the User Interface", + "releasedOn": "December 25, 2017", + "description": [ + "Once you define a component's initial state, you can display any part of it in the UI that is rendered. If a component is stateful, it will always have access to the data in{this.state.name}
\nstatein itsrender()method. You can access the data withthis.state.", + "If you want to access a state value within thereturnof the render method, you have to enclose the value in curly braces.", + "Stateis one of the most powerful features of components in React. It allows you to track important data in your app and render a UI in response to changes in this data. If your data changes, your UI will change. React uses what is called a virtual DOM, to keep track of changes behind the scenes. When state data updates, it triggers a re-render of the components using that data - including child components that received the data as a prop. React updates the actual DOM, but only where necessary. This means you don't have to worry about changing the DOM. You simply declare what the UI should look like.", + "Note that if you make a component stateful, no other components are aware of itsstate. Itsstateis completely encapsulated, or local to that component, unless you pass state data to a child component asprops. This notion of encapsulatedstateis very important because it allows you to write certain logic, then have that logic contained and isolated in one place in your code.", + "
", + "In the code editor,MyComponentis already stateful. Define anh1tag in the component's render method which renders the value ofnamefrom the component's state.", + "Note: Theh1should only render the value fromstateand nothing else. In JSX, any code you write with curly braces{ }will be treated as JavaScript. So to access the value fromstatejust enclose the reference in curly braces." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " name: 'freeCodeCamp'", + " }", + " }", + " render() {", + " return (", + "", + " { /* change code below this line */ }", + "", + " { /* change code above this line */ }", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "assert(Enzyme.mount(React.createElement(MyComponent)).state('name') === 'freeCodeCamp', 'message: MyComponentshould have a keynamewith valuefreeCodeCampstored in its state.');", + "assert(/.*<\\/h1><\\/div>/.test(Enzyme.mount(React.createElement(MyComponent)).html()), 'message:
MyComponentshould render anh1header enclosed in a singlediv.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ name: 'TestName' }); return waitForIt(() => mockedComponent.html()) }; const firstValue = await first(); assert(firstValue === '', 'message: The renderedTestName
h1header should contain text rendered from the component's state.'); };" + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n name: 'freeCodeCamp'\n }\n }\n render() {\n return (\n\n { /* change code below this line */ }\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036172", + "title": "Render State in the User Interface Another Way", + "releasedOn": "December 25, 2017", + "description": [ + "There is another way to access{this.state.name}
\n { /* change code above this line */ }\nstatein a component. In therender()method, before thereturnstatement, you can write JavaScript directly. For example, you could declare functions, access data fromstateorprops, perform computations on this data, and so on. Then, you can assign any data to variables, which you have access to in thereturnstatement.", + "
", + "In theMyComponentrender method, define aconstcallednameand set it equal to the name value in the component'sstate. Because you can write JavaScript directly in this part of the code, you don't have to enclose this reference in curly braces.", + "Next, in the return statement, render this value in anh1tag using the variablename. Remember, you need to use the JSX syntax (curly braces for JavaScript) in the return statement." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " name: 'freeCodeCamp'", + " }", + " }", + " render() {", + " // change code below this line", + "", + " // change code above this line", + " return (", + "", + " { /* change code below this line */ }", + "", + " { /* change code above this line */ }", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "assert(Enzyme.mount(React.createElement(MyComponent)).state('name') === 'freeCodeCamp', 'message: MyComponentshould have a keynamewith valuefreeCodeCampstored in its state.');", + "assert(/.*<\\/h1><\\/div>/.test(Enzyme.mount(React.createElement(MyComponent)).html()), 'message:
MyComponentshould render anh1header enclosed in a singlediv.');", + "getUserInput => assert(/\\n*\\s*\\{\\s*name\\s*\\}\\s*\\n*<\\/h1>/.test(getUserInput('index')), 'message: The rendered
h1tag should include a reference to{name}.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ name: 'TestName' }); return waitForIt(() => mockedComponent.html()) }; const firstValue = await first(); assert(firstValue === '', 'message: The renderedTestName
h1header should contain text rendered from the component's state.'); };" + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n name: 'freeCodeCamp'\n }\n }\n render() {\n // change code below this line\n const name = this.state.name;\n // change code above this line\n return (\n\n { /* change code below this line */ }\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036173", + "title": "Set State with this.setState", + "releasedOn": "December 25, 2017", + "description": [ + "The previous challenges covered component{name}
\n { /* change code above this line */ }\nstateand how to initialize state in theconstructor. There is also a way to change the component'sstate. React provides a method for updating componentstatecalledsetState. You call thesetStatemethod within your component class like so:this.setState(), passing in an object with key-value pairs. The keys are your state properties and the values are the updated state data. For instance, if we were storing ausernamein state and wanted to update it, it would look like this:", + "this.setState({", + "React expects you to never modify
username: 'Lewis'
});statedirectly, instead always usethis.setState()when state changes occur. Also, you should note that React may batch multiple state updates in order to improve performance. What this means is that state updates through thesetStatemethod can be asynchronous. There is an alternative syntax for thesetStatemethod which provides a way around this problem. This is rarely needed but it's good to keep it in mind! Please consult the React documentation for further details.", + "
", + "There is abuttonelement in the code editor which has anonClick()handler. This handler is triggered when thebuttonreceives a click event in the browser, and runs thehandleClickmethod defined onMyComponent. Within thehandleClickmethod, update the componentstateusingthis.setState(). Set thenameproperty instateto equal the stringReact Rocks!.", + "Click the button and watch the rendered state update. Don't worry if you don't fully understand how the click handler code works at this point. It's covered in upcoming challenges." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " name: 'Initial State'", + " };", + " this.handleClick = this.handleClick.bind(this);", + " }", + " handleClick() {", + " // change code below this line", + "", + " // change code above this line", + " }", + " render() {", + " return (", + "", + " ", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render({this.state.name}
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert(Enzyme.mount(React.createElement(MyComponent)).state('name') === 'Initial State', 'message: The state of MyComponentshould initialize with the key value pair{ name: Initial State }.');", + "assert(Enzyme.mount(React.createElement(MyComponent)).find('h1').length === 1, 'message:MyComponentshould render anh1header.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ name: 'TestName' }); return waitForIt(() => mockedComponent.html()); }; const firstValue = await first(); assert(/TestName<\\/h1>/.test(firstValue), 'message: The rendered
h1header should contain text rendered from the component's state.'); };", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ name: 'Before' }); return waitForIt(() => mockedComponent.state('name')); }; const second = () => { mockedComponent.setState({ name: 'React Rocks!' }); return waitForIt(() => mockedComponent.state('name')); }; const firstValue = await first(); const secondValue = await second(); assert(firstValue === 'Before' && secondValue === 'React Rocks!', 'message: Calling thehandleClickmethod onMyComponentshould set the name property in state to equalReact Rocks!.'); }; " + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n name: 'Initial State'\n };\n this.handleClick = this.handleClick.bind(this);\n }\n handleClick() {\n // change code below this line\n this.setState({\n name: 'React Rocks!'\n });\n // change code above this line\n }\n render() {\n return (\n\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036174", + "title": "Bind 'this' to a Class Method", + "releasedOn": "December 25, 2017", + "description": [ + "In addition to setting and updating{this.state.name}
\nstate, you can also define methods for your component class. A class method typically needs to use thethiskeyword so it can access properties on the class (such asstateandprops) inside the scope of the method. There are a few ways to allow your class methods to accessthis.", + "One common way is to explicitly bindthisin the constructor sothisbecomes bound to the class methods when the component is initialized. You may have noticed the last challenge usedthis.handleClick = this.handleClick.bind(this)for itshandleClickmethod in the constructor. Then, when you call a function likethis.setState()within your class method,thisrefers to the class and will not beundefined.", + "Note: Thethiskeyword is one of the most confusing aspects of JavaScript but it plays an important role in React. Although its behavior here is totally normal, these lessons aren't the place for an in-depth review ofthisso please refer to other lessons if the above is confusing!", + "
", + "The code editor has a component with astatethat keeps track of an item count. It also has a method which allows you to increment this item count. However, the method doesn't work because it's using thethiskeyword that is undefined. Fix it by explicitly bindingthisto theaddItem()method in the component's constructor.", + "Next, add a click handler to thebuttonelement in the render method. It should trigger theaddItem()method when the button receives a click event. Remember that the method you pass to theonClickhandler needs curly braces because it should be interpreted directly as JavaScript.", + "Once you complete the above steps you should be able to click the button and see the item count increment in the HTML." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " itemCount: 0", + " };", + " // change code below this line", + "", + " // change code above this line", + " }", + " addItem() {", + " this.setState({", + " itemCount: this.state.itemCount + 1", + " });", + " }", + " render() {", + " return (", + "", + " { /* change code below this line */ }", + " ", + " { /* change code above this line */ }", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Current Item Count: {this.state.itemCount}
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert(Enzyme.mount(React.createElement(MyComponent)).find('div').length === 1 && Enzyme.mount(React.createElement(MyComponent)).find('div').childAt(0).type() === 'button' && Enzyme.mount(React.createElement(MyComponent)).find('div').childAt(1).type() === 'h1', 'message: MyComponentshould return adivelement which wraps two elements, a button and anh1element, in that order.');", + "assert(Enzyme.mount(React.createElement(MyComponent)).state('itemCount') === 0, 'message: The state ofMyComponentshould initialize with the key value pair{ itemCount: 0 }.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ itemCount: 0 }); return waitForIt(() => mockedComponent.state('itemCount')); }; const second = () => { mockedComponent.find('button').simulate('click'); return waitForIt(() => mockedComponent.state('itemCount')); }; const firstValue = await first(); const secondValue = await second(); assert(firstValue === 0 && secondValue === 1, 'message: Clicking thebuttonelement should run theaddItemmethod and increment the stateitemCountby1.'); };" + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n itemCount: 0\n };\n this.addItem = this.addItem.bind(this);\n }\n addItem() {\n this.setState({\n itemCount: this.state.itemCount + 1\n });\n }\n render() {\n return (\n\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036176", + "title": "Use State to Toggle an Element", + "releasedOn": "December 25, 2017", + "description": [ + "You can useCurrent Item Count: {this.state.itemCount}
\nstatein React applications in more complex ways than what you've seen so far. One example is to monitor the status of a value, then render the UI conditionally based on this value. There are several different ways to accomplish this, and the code editor shows one method.", + "
", + "MyComponenthas avisibilityproperty which is initialized tofalse. The render method returns one view if the value ofvisibilityis true, and a different view if it is false.", + "Currently, there is no way of updating thevisibilityproperty in the component'sstate. The value should toggle back and forth between true and false. There is a click handler on the button which triggers a class method calledtoggleVisibility(). Define this method so thestateofvisibilitytoggles to the opposite value when the method is called. Ifvisibilityisfalse, the method sets it totrue, and vice versa.", + "Finally, click the button to see the conditional rendering of the component based on itsstate.", + "Hint: Don't forget to bind thethiskeyword to the method in the constructor!" + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " visibility: false", + " };", + " // change code below this line", + "", + " // change code above this line", + " }", + " // change code below this line", + "", + " // change code above this line", + " render() {", + " if (this.state.visibility) {", + " return (", + "", + " ", + "", + " );", + " } else {", + " return (", + "Now you see me!
", + "", + " ", + "", + " );", + " }", + " }", + "};" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "assert.strictEqual(Enzyme.mount(React.createElement(MyComponent)).find('div').find('button').length, 1, 'message: MyComponentshould return adivelement which contains abutton.');", + "assert.strictEqual(Enzyme.mount(React.createElement(MyComponent)).state('visibility'), false, 'message: The state ofMyComponentshould initialize with avisibilityproperty set tofalse.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ visibility: false }); return waitForIt(() => mockedComponent.state('visibility')); }; const second = () => { mockedComponent.find('button').simulate('click'); return waitForIt(() => mockedComponent.state('visibility')); }; const third = () => { mockedComponent.find('button').simulate('click'); return waitForIt(() => mockedComponent.state('visibility')); }; const firstValue = await first(); const secondValue = await second(); const thirdValue = await third(); assert(!firstValue && secondValue && !thirdValue, 'message: Clicking the button element should toggle thevisibilityproperty in state betweentrueandfalse.'); }; " + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n visibility: false\n };\n this.toggleVisibility = this.toggleVisibility.bind(this);\n }\n toggleVisibility() {\n this.setState({\n visibility: !this.state.visibility\n });\n }\n render() {\n if (this.state.visibility) {\n return (\n\n \n\n );\n } else {\n return (\nNow you see me!
\n\n \n\n );\n }\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036177", + "title": "Write a Simple Counter", + "releasedOn": "December 25, 2017", + "description": [ + "You can design a more complex stateful component by combining the concepts covered so far. These include initializingstate, writing methods that setstate, and assigning click handlers to trigger these methods.", + "
", + "TheCountercomponent keeps track of acountvalue instate. There are two buttons which call methodsincrement()anddecrement(). Write these methods so the counter value is incremented or decremented by 1 when the appropriate button is clicked. Also, create areset()method so when the reset button is clicked, the count is set to 0.", + "Note: Make sure you don't modify theclassNamesof the buttons." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class Counter extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " count: 0", + " };", + " }", + " // change code below this line", + "", + " // change code above this line", + " render() {", + " return (", + "", + " ", + " ", + " ", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Current Count: {this.state.count}
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((() => { const mockedComponent = Enzyme.mount(React.createElement(Counter)); return (mockedComponent.find('.inc').text() === 'Increment!' && mockedComponent.find('.dec').text() === 'Decrement!' && mockedComponent.find('.reset').text() === 'Reset'); })(), 'message: MyComponentshould return adivelement which contains three buttons with text content in this orderIncrement!,Decrement!,Reset.');", + "assert.strictEqual(Enzyme.mount(React.createElement(Counter)).state('count'), 0, 'message: The state ofMyComponentshould initialize with acountproperty set to0.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(Counter)); const first = () => { mockedComponent.setState({ count: 0 }); return waitForIt(() => mockedComponent.state('count')); }; const second = () => { mockedComponent.find('.inc').simulate('click'); return waitForIt(() => mockedComponent.state('count')); }; const firstValue = await first(); const secondValue = await second(); assert(firstValue === 0 && secondValue === 1, 'message: Clicking the increment button should increment the count by1.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(Counter)); const first = () => { mockedComponent.setState({ count: 0 }); return waitForIt(() => mockedComponent.state('count')); }; const second = () => { mockedComponent.find('.dec').simulate('click'); return waitForIt(() => mockedComponent.state('count')); }; const firstValue = await first(); const secondValue = await second(); assert(firstValue === 0 && secondValue === -1, 'message: Clicking the decrement button should decrement the count by1.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(Counter)); const init = () => { mockedComponent.setState({ count: 0 }); return waitForIt(() => mockedComponent.state('count')); }; const increment = () => { mockedComponent.find('.inc').simulate('click'); mockedComponent.find('.inc').simulate('click'); return waitForIt(() => mockedComponent.state('count')); }; const decrement = () => { mockedComponent.find('.dec').simulate('click'); return waitForIt(() => mockedComponent.state('count')); }; const reset = () => { mockedComponent.find('.reset').simulate('click'); return waitForIt(() => mockedComponent.state('count')); }; const firstValue = await init(); const secondValue = await increment(); const thirdValue = await decrement(); const fourthValue = await reset(); assert(firstValue === 0 && secondValue === 2 && thirdValue === 1 && fourthValue === 0, 'message: Clicking the reset button should reset the count to0.'); }; " + ], + "solutions": [ + "class Counter extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n count: 0\n };\n this.increment = this.increment.bind(this);\n this.decrement = this.decrement.bind(this);\n this.reset = this.reset.bind(this);\n }\n reset() {\n this.setState({\n count: 0\n });\n }\n increment() {\n this.setState({\n count: this.state.count + 1\n });\n }\n decrement() {\n this.setState({\n count: this.state.count - 1\n });\n }\n render() {\n return (\n\n \n \n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036178", + "title": "Create a Controlled Input", + "releasedOn": "December 25, 2017", + "description": [ + "Your application may have more complex interactions betweenCurrent Count: {this.state.count}
\nstateand the rendered UI. For example, form control elements for text input, such asinputandtextarea, maintain their own state in the DOM as the user types. With React, you can move this mutatable state into a React component'sstate. The user's input becomes part of the applicationstate, so React controls the value of that input field. Typically, if you have React components with input fields the user can type into, it will be a controlled input form.", + "
", + "The code editor has the skeleton of a component calledControlledInputto create a controlledinputelement. The component'sstateis already initialized with aninputproperty that holds an empty string. This value represents the text a user types into theinputfield.", + "First, create a method calledhandleChange()that has a parameter calledevent. When the method is called, it receives aneventobject that contains a string of text from theinputelement. You can access this string withevent.target.valueinside the method. Update theinputproperty of the component'sstatewith this new string.", + "In the render method, create theinputelement above theh4tag. Add avalueattribute which is equal to theinputproperty of the component'sstate. Then add anonChange()event handler set to thehandleChange()method.", + "When you type in the input box, that text is processed by thehandleChange()method, set as theinputproperty in the localstate, and rendered as the value in theinputbox on the page. The componentstateis the single source of truth regarding the input data." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class ControlledInput extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " input: ''", + " };", + " }", + " // change code below this line", + "", + " // change code above this line", + " render() {", + " return (", + "", + " { /* change code below this line */}", + "", + " { /* change code above this line */}", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Controlled Input:
", + "{this.state.input}
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert(Enzyme.mount(React.createElement(ControlledInput)).find('div').children().find('input').length === 1 && Enzyme.mount(React.createElement(ControlledInput)).find('div').children().find('p').length === 1, 'message: ControlledInputshould return adivelement which contains aninputand aptag.');", + "assert.strictEqual(Enzyme.mount(React.createElement(ControlledInput)).state('input'), '', 'message: The state ofControlledInputshould initialize with aninputproperty set to an empty string.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(ControlledInput)); const _1 = () => { mockedComponent.setState({ input: '' }); return waitForIt(() => mockedComponent.state('input'))}; const _2 = () => { mockedComponent.find('input').simulate('change', { target: { value: 'TestInput' }}); return waitForIt(() => ({ state: mockedComponent.state('input'), text: mockedComponent.find('p').text(), inputVal: mockedComponent.find('input').props().value }))}; const before = await _1(); const after = await _2(); assert(before === '' && after.state === 'TestInput' && after.text === 'TestInput' && after.inputVal === 'TestInput', 'message: Typing in the input element should update the state and the value of the input, and thepelement should render this state as you type.'); }; " + ], + "solutions": [ + "class ControlledInput extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n input: ''\n };\n this.handleChange = this.handleChange.bind(this);\n }\n handleChange(event) {\n this.setState({\n input: event.target.value\n });\n }\n render() {\n return (\n\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036179", + "title": "Create a Controlled Form", + "releasedOn": "December 25, 2017", + "description": [ + "The last challenge showed that React can control the internal state for certain elements likeControlled Input:
\n\n{this.state.input}
\ninputandtextarea, which makes them controlled components. This applies to other form elements as well, including the regular HTMLformelement.", + "
", + "TheMyFormcomponent is set up with an emptyformwith a submit handler. The submit handler will be called when the formed is submitted.", + "We've added a button which submits the form. You can see it has thetypeset tosubmitindicating it is the button controlling the form. Add theinputelement in theformand set itsvalueandonChange()attributes like the last challenge. You should then complete thehandleSubmitmethod so that it sets the component state propertysubmitto the current input value in the localstate.", + "Note: You also must callevent.preventDefault()in the submit handler, to prevent the default form submit behavior which will refresh the web page.", + "Finally, create anh1tag after theformwhich renders thesubmitvalue from the component'sstate. You can then type in the form and click the button (or press enter), and you should see your input rendered to the page." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyForm extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " input: '',", + " submit: ''", + " };", + " this.handleChange = this.handleChange.bind(this);", + " this.handleSubmit = this.handleSubmit.bind(this);", + " }", + " handleChange(event) {", + " this.setState({", + " input: event.target.value", + " });", + " }", + " handleSubmit(event) {", + " // change code below this line", + "", + " // change code above this line", + " }", + " render() {", + " return (", + "", + " ", + " { /* change code below this line */ }", + "", + " { /* change code above this line */ }", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "assert((() => { const mockedComponent = Enzyme.mount(React.createElement(MyForm)); return (mockedComponent.find('div').children().find('form').length === 1 && mockedComponent.find('div').children().find('h1').length === 1 && mockedComponent.find('form').children().find('input').length === 1 && mockedComponent.find('form').children().find('button').length === 1) })(), 'message: MyFormshould return adivelement which contains aformand anh1tag. The form should include aninputand abutton.');", + "assert(Enzyme.mount(React.createElement(MyForm)).state('input') === '' && Enzyme.mount(React.createElement(MyForm)).state('submit') === '', 'message: The state ofMyFormshould initialize withinputandsubmitproperties, both set to empty strings.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyForm)); const _1 = () => { mockedComponent.setState({ input: '' }); return waitForIt(() => mockedComponent.state('input'))}; const _2 = () => { mockedComponent.find('input').simulate('change', { target: { value: 'TestInput' }}); return waitForIt(() => ({ state: mockedComponent.state('input'), inputVal: mockedComponent.find('input').props().value }))}; const before = await _1(); const after = await _2(); assert(before === '' && after.state === 'TestInput' && after.inputVal === 'TestInput', 'message: Typing in theinputelement should update theinputproperty of the component's state.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyForm)); const _1 = () => { mockedComponent.setState({ input: '' }); mockedComponent.setState({submit: ''}); mockedComponent.find('input').simulate('change', {target: {value: 'SubmitInput'}}); return waitForIt(() => mockedComponent.state('submit'))}; const _2 = () => { mockedComponent.find('form').simulate('submit'); return waitForIt(() => mockedComponent.state('submit'))}; const before = await _1(); const after = await _2(); assert(before === '' && after === 'SubmitInput', 'message: Submitting the form should runhandleSubmitwhich should set thesubmitproperty in state equal to the current input.'); };", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyForm)); const _1 = () => { mockedComponent.setState({ input: '' }); mockedComponent.setState({submit: ''}); mockedComponent.find('input').simulate('change', {target: {value: 'TestInput'}}); return waitForIt(() => mockedComponent.find('h1').text())}; const _2 = () => { mockedComponent.find('form').simulate('submit'); return waitForIt(() => mockedComponent.find('h1').text())}; const before = await _1(); const after = await _2(); assert(before === '' && after === 'TestInput', 'message: Theh1header should render the value of thesubmitfield from the component's state.'); }; " + ], + "solutions": [ + "class MyForm extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n input: '',\n submit: ''\n };\n this.handleChange = this.handleChange.bind(this);\n this.handleSubmit = this.handleSubmit.bind(this);\n }\n handleChange(event) {\n this.setState({\n input: event.target.value\n });\n }\n handleSubmit(event) {\n event.preventDefault()\n this.setState({\n submit: this.state.input\n });\n }\n render() {\n return (\n\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403617a", + "title": "Pass State as Props to Child Components", + "releasedOn": "December 25, 2017", + "description": [ + "You saw a lot of examples that passed props to child JSX elements and child React components in previous challenges. You may be wondering where those props come from. A common pattern is to have a stateful component containing the{this.state.submit}
\nstateimportant to your app, that then renders child components. You want these components to have access to some pieces of thatstate, which are passed in as props.", + "For example, maybe you have anAppcomponent that renders aNavbar, among other components. In yourApp, you havestatethat contains a lot of user information, but theNavbaronly needs access to the user's username so it can display it. You pass that piece ofstateto theNavbarcomponent as a prop.", + "This pattern illustrates some important paradigms in React. The first is unidirectional data flow. State flows in one direction down the tree of your application's components, from the stateful parent component to child components. The child components only receive the state data they need. The second is that complex stateful apps can be broken down into just a few, or maybe a single, stateful component. The rest of your components simply receive state from the parent as props, and render a UI from that state. It begins to create a separation where state management is handled in one part of code and UI rendering in another. This principle of separating state logic from UI logic is one of React's key principles. When it's used correctly, it makes the design of complex, stateful applications much easier to manage.", + "
", + "TheMyAppcomponent is stateful and renders aNavbarcomponent as a child. Pass thenameproperty in itsstatedown to the child component, then show thenamein theh1tag that's part of theNavbarrender method." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyApp extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " name: 'CamperBot'", + " }", + " }", + " render() {", + " return (", + "", + "", + " );", + " }", + "};", + "", + "class Navbar extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " render() {", + " return (", + "", + " ", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Hello, my name is: /* your code here */
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyApp)); return mockedComponent.find('MyApp').length === 1 && mockedComponent.find('Navbar').length === 1; })(), 'message: The MyAppcomponent should render with aNavbarcomponent inside.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyApp)); const setState = () => { mockedComponent.setState({name: 'TestName'}); return waitForIt(() => mockedComponent.find('Navbar').props() )}; const navProps = await setState(); assert(navProps.name === 'TestName', 'message: TheNavbarcomponent should receive theMyAppstate propertynameas props.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyApp)); const navH1Before = mockedComponent.find('Navbar').find('h1').text(); const setState = () => { mockedComponent.setState({name: 'TestName'}); return waitForIt(() => mockedComponent.find('Navbar').find('h1').text() )}; const navH1After = await setState(); assert(new RegExp('TestName').test(navH1After) && navH1After !== navH1Before, 'message: Theh1element inNavbarshould render thenameprop.'); }; " + ], + "solutions": [ + "class MyApp extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n name: 'CamperBot'\n }\n }\n render() {\n return (\n\n\n );\n }\n};\nclass Navbar extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\n\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403617b", + "title": "Pass a Callback as Props", + "releasedOn": "December 25, 2017", + "description": [ + "You can passHello, my name is: {this.props.name}
\nstateas props to child components, but you're not limited to passing data. You can also pass handler functions or any method that's defined on a React component to a child component. This is how you allow child components to interact with their parent components. You pass methods to a child just like a regular prop. It's assigned a name and you have access to that method name underthis.propsin the child component.", + "
", + "There are three components outlined in the code editor. TheMyAppcomponent is the parent that will render theGetInputandRenderInputchild components. Add theGetInputcomponent to the render method inMyApp, then pass it a prop calledinputassigned toinputValuefromMyApp'sstate. Also create a prop calledhandleChangeand pass the input handlerhandleChangeto it.", + "Next, addRenderInputto the render method inMyApp, then create a prop calledinputand pass theinputValuefromstateto it. Once you are finished you will be able to type in theinputfield in theGetInputcomponent, which then calls the handler method in its parent via props. This updates the input in thestateof the parent, which is passed as props to both children. Observe how the data flows between the components and how the single source of truth remains thestateof the parent component. Admittedly, this example is a bit contrived, but should serve to illustrate how data and callbacks can be passed between React components." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyApp extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " inputValue: ''", + " }", + " this.handleChange = this.handleChange.bind(this);", + " }", + " handleChange(event) {", + " this.setState({", + " inputValue: event.target.value", + " });", + " }", + " render() {", + " return (", + "", + " { /* change code below this line */ }", + "", + " { /* change code above this line */ }", + "", + " );", + " }", + "};", + "", + "class GetInput extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " render() {", + " return (", + "", + "", + " );", + " }", + "};", + "", + "class RenderInput extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " render() {", + " return (", + "Get Input:
", + " ", + "", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Input Render:
", + "{this.props.input}
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyApp)); return mockedComponent.find('MyApp').length === 1; })(), 'message: The MyAppcomponent should render.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyApp)); return mockedComponent.find('GetInput').length === 1; })(), 'message: TheGetInputcomponent should render.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyApp)); return mockedComponent.find('RenderInput').length === 1; })(), 'message: TheRenderInputcomponent should render.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyApp)); const state_1 = () => { mockedComponent.setState({inputValue: ''}); return waitForIt(() => mockedComponent.state() )}; const state_2 = () => { mockedComponent.find('input').simulate('change', {target: {value: 'TestInput'}}); return waitForIt(() => mockedComponent.state() )}; const updated_1 = await state_1(); const updated_2 = await state_2(); assert(updated_1.inputValue === '' && updated_2.inputValue === 'TestInput', 'message: TheGetInputcomponent should receive theMyAppstate propertyinputValueas props and contain aninputelement which modifiesMyAppstate.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyApp)); const state_1 = () => { mockedComponent.setState({inputValue: 'TestName'}); return waitForIt(() => mockedComponent )}; const updated_1 = await state_1(); assert(updated_1.find('p').text().includes('TestName'), 'message: TheRenderInputcomponent should receive theMyAppstate propertyinputValueas props.'); }; " + ], + "solutions": [ + "class MyApp extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n inputValue: ''\n }\n this.handleChange = this.handleChange.bind(this); \n }\n handleChange(event) {\n this.setState({\n inputValue: event.target.value\n });\n }\n render() {\n return (\n\n\n );\n }\n};\n\nclass GetInput extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\n\n \n \n\n );\n }\n};\n\nclass RenderInput extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\nGet Input:
\n \n\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403617c", + "title": "Use the Lifecycle Method componentWillMount", + "releasedOn": "December 25, 2017", + "description": [ + "React components have several special methods that provide opportunities to perform actions at specific points in the lifecycle of a component. These are called lifecycle methods, or lifecycle hooks, and allow you to catch components at certain points in time. This can be before they are rendered, before they update, before they receive props, before they unmount, and so on. Here is a list of some of the main lifecycle methods:", + "Input Render:
\n{this.props.input}
\ncomponentWillMount()", + "componentDidMount()", + "componentWillReceiveProps()", + "shouldComponentUpdate()", + "componentWillUpdate()", + "componentDidUpdate()", + "componentWillUnmount()", + "The next several lessons will cover some of the basic use cases for these lifecycle methods.", + "
", + "ThecomponentWillMount()method is called before therender()method when a component is being mounted to the DOM. Log something to the console withincomponentWillMount()- you may want to have your browser console open to see the output." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " componentWillMount() {", + " // change code below this line", + "", + " // change code above this line", + " }", + " render() {", + " return ", + " }", + "};" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.find('div').length === 1; })(), 'message: MyComponentshould render adivelement.');", + "assert((function() { const lifecycle = React.createElement(MyComponent).type.prototype.componentWillMount.toString().replace(/ /g,''); return lifecycle.includes('console.log('); })(), 'message:console.logshould be called incomponentWillMount.');" + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n }\n componentWillMount() {\n // change code below this line\n console.log('Component is mounting...');\n // change code above this line\n }\n render() {\n return \n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403617d", + "title": "Use the Lifecycle Method componentDidMount", + "releasedOn": "December 25, 2017", + "description": [ + "Most web developers, at some point, need to call an API endpoint to retrieve data. If you're working with React, it's important to know where to perform this action.", + "The best practice with React is to place API calls or any calls to your server in the lifecycle methodcomponentDidMount(). This method is called after a component is mounted to the DOM. Any calls tosetState()here will trigger a re-rendering of your component. When you call an API in this method, and set your state with the data that the API returns, it will automatically trigger an update once you receive the data.", + "
", + "There is a mock API call incomponentDidMount(). It sets state after 2.5 seconds to simulate calling a server to retrieve data. This example requests the current total active users for a site. In the render method, render the value ofactiveUsersin theh1. Watch what happens in the preview, and feel free to change the timeout to see the different effects." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " activeUsers: null", + " };", + " }", + " componentDidMount() {", + " setTimeout( () => {", + " this.setState({", + " activeUsers: 1273", + " });", + " }, 2500);", + " }", + " render() {", + " return (", + "", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Active Users: { /* change code here */ }
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((() => { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return (mockedComponent.find('div').length === 1 && mockedComponent.find('h1').length === 1); })(), 'message: MyComponentshould render adivelement which wraps anh1tag.');", + "assert((() => { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return new RegExp('setTimeout(.|\\n)+setState(.|\\n)+activeUsers').test(String(mockedComponent.instance().componentDidMount)); })(), 'message: Component state should be updated with a timeout function incomponentDidMount.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const first = () => { mockedComponent.setState({ activeUsers: 1237 }); return waitForIt(() => mockedComponent.find('h1').text()); }; const second = () => { mockedComponent.setState({ activeUsers: 1000 }); return waitForIt(() => mockedComponent.find('h1').text()); }; const firstValue = await first(); const secondValue = await second(); assert(new RegExp('1237').test(firstValue) && new RegExp('1000').test(secondValue), 'message: Theh1tag should render theactiveUsersvalue fromMyComponent's state.); }; " + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n activeUsers: null\n };\n }\n componentDidMount() {\n setTimeout( () => {\n this.setState({\n activeUsers: 1273\n });\n }, 2500);\n }\n render() {\n return (\n\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403617e", + "title": "Add Event Listeners", + "releasedOn": "December 25, 2017", + "description": [ + "TheActive Users: {this.state.activeUsers}
\ncomponentDidMount()method is also the best place to attach any event listeners you need to add for specific functionality. React provides a synthetic event system which wraps the native event system present in browsers. This means that the synthetic event system behaves exactly the same regardless of the user's browser - even if the native events may behave differently between different browsers.", + "You've already been using some of these synthetic event handlers such asonClick(). React's synthetic event system is great to use for most interactions you'll manage on DOM elements. However, if you want to attach an event handler to the document or window objects, you have to do this directly.", + "
", + "Attach an event listener in thecomponentDidMount()method forkeydownevents and have these events trigger the callbackhandleKeyPress(). You can usedocument.addEventListener()which takes the event (in quotes) as the first argument and the callback as the second argument.", + "Then, incomponentWillUnmount(), remove this same event listener. You can pass the same arguments todocument.removeEventListener(). It's good practice to use this lifecycle method to do any clean up on React components before they are unmounted and destroyed. Removing event listeners is an example of one such clean up action." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " message: ''", + " };", + " this.handleEnter = this.handleEnter.bind(this);", + " this.handleKeyPress = this.handleKeyPress.bind(this);", + " }", + " // change code below this line", + " componentDidMount() {", + "", + " }", + " componentWillUnmount() {", + "", + " }", + " // change code above this line", + " handleEnter() {", + " this.setState({", + " message: this.state.message + 'You pressed the enter key! '", + " });", + " }", + " handleKeyPress(event) {", + " if (event.keyCode === 13) {", + " this.handleEnter();", + " }", + " }", + " render() {", + " return (", + "", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render({this.state.message}
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((() => { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.find('div').children().find('h1').length === 1; })(), 'message: MyComponentshould render adivelement which wraps anh1tag.');", + "assert((() => { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const didMountString = mockedComponent.instance().componentDidMount.toString(); return new RegExp('document\\.addEventListener(.|\\n|\\r)+keydown(.|\\n|\\r)+this\\.handleKeyPress').test(didMountString); })(), 'message: A keydown listener should be attached to the document incomponentDidMount.');", + "assert((() => { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const willUnmountString = mockedComponent.instance().componentWillUnmount.toString(); return new RegExp('document\\.removeEventListener(.|\\n|\\r)+keydown(.|\\n|\\r)+this\\.handleKeyPress').test(willUnmountString); })(), 'message: The keydown listener should be removed from the document incomponentWillUnmount.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const beforeState = mockedComponent.state('message'); const beforeText = mockedComponent.find('h1').text(); const pressEnterKey = () => { mockedComponent.instance().handleKeyPress({ keyCode: 13 }); return waitForIt(() => { mockedComponent.update(); return { state: mockedComponent.state('message'), text: mockedComponent.find('h1').text()}; });}; const afterKeyPress = await pressEnterKey(); assert(beforeState !== afterKeyPress.state && beforeText !== afterKeyPress.text, 'message: Once the component has mounted, pressingentershould update its state and the renderedh1tag.'); }; " + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n message: ''\n };\n this.handleKeyPress = this.handleKeyPress.bind(this);\n this.handleEnter = this.handleEnter.bind(this); }\n componentDidMount() {\n // change code below this line\n document.addEventListener('keydown', this.handleKeyPress);\n // change code above this line\n }\n componentWillUnmount() {\n // change code below this line\n document.removeEventListener('keydown', this.handleKeyPress);\n // change code above this line\n }\n handleEnter() {\n this.setState({\n message: this.state.message + 'You pressed the enter key! '\n });\n }\n handleKeyPress(event) {\n if (event.keyCode === 13) {\n this.handleEnter();\n }\n }\n render() {\n return (\n\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403617f", + "title": "Manage Updates with Lifecycle Methods", + "releasedOn": "December 25, 2017", + "description": [ + "Another lifecycle method is{this.state.message}
\ncomponentWillReceiveProps()which is called whenever a component is receiving new props. This method receives the new props as an argument, which is usually written asnextProps. You can use this argument and compare withthis.propsand perform actions before the component updates. For example, you may callsetState()locally before the update is processed.", + "Another method iscomponentDidUpdate(), and is called immediately after a component re-renders. Note that rendering and mounting are considered different things in the component lifecycle. When a page first loads, all components are mounted and this is where methods likecomponentWillMount()andcomponentDidMount()are called. After this, as state changes, components re-render themselves. The next challenge covers this in more detail.", + "
", + "The child componentDialogreceivesmessageprops from its parent, theControllercomponent. Write thecomponentWillReceiveProps()method in theDialogcomponent and have it logthis.propsandnextPropsto the console. You'll need to passnextPropsas an argument to this method and although it's possible to name it anything, name itnextPropshere.", + "Next, addcomponentDidUpdate()in theDialogcomponent, and log a statement that says the component has updated. This method works similar tocomponentWillUpdate(), which is provided for you. Now click the button to change the message and watch your browser console. The order of the console statements show the order the methods are called.", + "Note: You'll need to write the lifecycle methods as normal functions and not as arrow functions to pass the tests (there is also no advantage to writing lifecycle methods as arrow functions)." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class Dialog extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " componentWillUpdate() {", + " console.log('Component is about to update...');", + " }", + " // change code below this line", + "", + " // change code above this line", + " render() {", + " return{this.props.message}
", + " }", + "};", + "", + "class Controller extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " message: 'First Message'", + " };", + " this.changeMessage = this.changeMessage.bind(this);", + " }", + " changeMessage() {", + " this.setState({", + " message: 'Second Message'", + " });", + " }", + " render() {", + " return (", + "", + " ", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(Controller)); return mockedComponent.find('Controller').length === 1 && mockedComponent.find('Dialog').length === 1; })(), 'message: The Controllercomponent should render theDialogcomponent as a child.');", + "assert((function() { const lifecycleChild = React.createElement(Dialog).type.prototype.componentWillReceiveProps.toString().replace(/ /g,''); return lifecycleChild.includes('console.log') && lifecycleChild.includes('this.props') })(), 'message: ThecomponentWillReceivePropsmethod in theDialogcomponent should logthis.propsto the console.');", + "assert((function() { const lifecycleChild = React.createElement(Dialog).type.prototype.componentWillReceiveProps.toString().replace(/ /g,''); const nextPropsAsParameterTest = \/componentWillReceiveProps(| *?= *?)(\\(|)nextProps(\\)|)( *?=> *?{| *?{|{)\/; const nextPropsInConsoleLogTest = \/console\\.log\\(.*?nextProps\\b.*?\\)\/; return ( lifecycleChild.includes('console.log') && nextPropsInConsoleLogTest.test(lifecycleChild) && nextPropsAsParameterTest.test(lifecycleChild) ); })(), 'message: ThecomponentWillReceivePropsmethod in theDialogcomponent should lognextPropsto the console.');", + "assert((function() { const lifecycleChild = React.createElement(Dialog).type.prototype.componentDidUpdate.toString().replace(/ /g,''); return lifecycleChild.length !== 'undefined' && lifecycleChild.includes('console.log'); })(), 'message: TheDialogcomponent should call thecomponentDidUpdatemethod and log a message to the console.');" + ], + "solutions": [ + "class Dialog extends React.Component {\n constructor(props) {\n super(props);\n }\n componentWillUpdate() {\n console.log('Component is about to update...');\n }\n // change code below this line\n componentWillReceiveProps(nextProps) {\n console.log(this.props, nextProps);\n }\n componentDidUpdate() {\n console.log('Component re-rendered');\n }\n // change code above this line\n render() {\n return{this.props.message}
\n }\n};\n\nclass Controller extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n message: 'First Message'\n };\n this.changeMessage = this.changeMessage.bind(this); \n }\n changeMessage() {\n this.setState({\n message: 'Second Message'\n });\n }\n render() {\n return (\n\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036180", + "title": "Optimize Re-Renders with shouldComponentUpdate", + "releasedOn": "December 25, 2017", + "description": [ + "So far, if any component receives newstateor newprops, it re-renders itself and all its children. This is usually okay. But React provides a lifecycle method you can call when child components receive newstateorprops, and declare specifically if the components should update or not. The method isshouldComponentUpdate(), and it takesnextPropsandnextStateas parameters.", + "This method is a useful way to optimize performance. For example, the default behavior is that your component re-renders when it receives newprops, even if thepropshaven't changed. You can useshouldComponentUpdate()to prevent this by comparing theprops. The method must return abooleanvalue that tells React whether or not to update the component. You can compare the current props (this.props) to the next props (nextProps) to determine if you need to update or not, and returntrueorfalseaccordingly.", + "
", + "TheshouldComponentUpdate()method is added in a component calledOnlyEvens. Currently, this method returnstruesoOnlyEvensre-renders every time it receives newprops. Modify the method soOnlyEvensupdates only if thevalueof its new props is even. Click theAddbutton and watch the order of events in your browser's console as the other lifecycle hooks are triggered." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class OnlyEvens extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " shouldComponentUpdate(nextProps, nextState) {", + " console.log('Should I update?');", + " // change code below this line", + " return true;", + " // change code above this line", + " }", + " componentWillReceiveProps(nextProps) {", + " console.log('Receiving new props...');", + " }", + " componentDidUpdate() {", + " console.log('Component re-rendered.');", + " }", + " render() {", + " return{this.props.value}
", + " }", + "};", + "", + "class Controller extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " value: 0", + " };", + " this.addValue = this.addValue.bind(this);", + " }", + " addValue() {", + " this.setState({", + " value: this.state.value + 1", + " });", + " }", + " render() {", + " return (", + "", + " ", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(", + " , document.getElementById('root'))" + } + }, + "tests": [ + "assert((() => { const mockedComponent = Enzyme.mount(React.createElement(Controller)); return mockedComponent.find('Controller').length === 1 && mockedComponent.find('OnlyEvens').length === 1; })(), 'message: The Controllercomponent should render theOnlyEvenscomponent as a child.');", + "assert((() => { const child = React.createElement(OnlyEvens).type.prototype.shouldComponentUpdate.toString().replace(/s/g,''); return child !== 'undefined'; })(), 'message: TheshouldComponentUpdatemethod should be defined on theOnlyEvenscomponent.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(Controller)); const first = () => { mockedComponent.setState({ value: 1000 }); return waitForIt(() => mockedComponent.find('h1').html()); }; const second = () => { mockedComponent.setState({ value: 10 }); return waitForIt(() => mockedComponent.find('h1').html()); }; const firstValue = await first(); const secondValue = await second(); assert(firstValue === '1000
' && secondValue === '10
', 'message: TheOnlyEvenscomponent should return anh1tag which renders the value ofthis.'); }; props.value.", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(Controller)); const first = () => { mockedComponent.setState({ value: 8 }); return waitForIt(() => mockedComponent.find('h1').text()); }; const second = () => { mockedComponent.setState({ value: 7 }); return waitForIt(() => mockedComponent.find('h1').text()); }; const third = () => { mockedComponent.setState({ value: 42 }); return waitForIt(() => mockedComponent.find('h1').text()); }; const firstValue = await first(); const secondValue = await second(); const thirdValue = await third(); assert(firstValue === '8' && secondValue === '8' && thirdValue === '42', 'message:OnlyEvensshould re-render only whennextProps.'); }; valueis even." + ], + "solutions": [ + "class OnlyEvens extends React.Component {\n constructor(props) {\n super(props);\n }\n shouldComponentUpdate(nextProps, nextState) {\n console.log('Should I update?');\n // change code below this line\n return nextProps.value % 2 === 0;\n // change code above this line\n }\n componentWillReceiveProps(nextProps) {\n console.log('Receiving new props...');\n }\n componentDidUpdate() {\n console.log('Component re-rendered.');\n }\n render() {\n return{this.props.value}
\n }\n};\n\nclass Controller extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n value: 0\n };\n this.addValue = this.addValue.bind(this);\n }\n addValue() {\n this.setState({\n value: this.state.value + 1\n });\n }\n render() {\n return (\n\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036181", + "title": "Introducing Inline Styles", + "releasedOn": "December 25, 2017", + "description": [ + "There are other complex concepts that add powerful capabilities to your React code. But you may be wondering about the more simple problem of how to style those JSX elements you create in React. You likely know that it won't be exactly the same as working with HTML because of the way you apply classes to JSX elements.", + "If you import styles from a stylesheet, it isn't much different at all. You apply a class to your JSX element using the\n classNameattribute, and apply styles to the class in your stylesheet. Another option is to apply inline styles, which are very common in ReactJS development.", + "You apply inline styles to JSX elements similar to how you do it in HTML, but with a few JSX differences. Here's an example of an inline style in HTML:", + "<div style=\"color: yellow; font-size: 16px\">Mellow Yellow</div>", + "JSX elements use thestyleattribute, but because of the way JSX is transpiled, you can't set the value to astring. Instead, you set it equal to a JavaScriptobject. Here's an example:", + "<div style={{color: \"yellow\", fontSize: 16}}>Mellow Yellow</div>", + "
", + "Add astyleattribute to thedivin the code editor to give the text a color of red and font size of 72px.", + "Note that you can optionally set the font size to be a number, omitting the units \"px\", or write it as \"72px\"." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "", + "class Colorful extends React.Component {", + " render() {", + " return (", + "Big Red", + " );", + " }", + "};", + "" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(Colorful)); return mockedComponent.children().type() === 'div'; })(), 'message: The component should render a divelement.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(Colorful)); return mockedComponent.children().props().style.color === 'red'; })(), 'message: Thedivelement should have a color ofred.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(Colorful)); return (mockedComponent.children().props().style.fontSize === 72 || mockedComponent.children().props().style.fontSize === '72' || mockedComponent.children().props().style.fontSize === '72px'); })(), 'message: Thedivelement should have a font size of72px.');" + ], + "solutions": [ + "\nclass Colorful extends React.Component {\n render() {\n return (\nBig Red\n );\n }\n};\n" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036182", + "title": "Add Inline Styles in React", + "releasedOn": "December 25, 2017", + "description": [ + "You may have noticed in the last challenge that there were several other syntax differences from HTML inline styles in addition to thestyleattribute set to a JavaScript object. First, the names of certain CSS style properties use camel case. For example, the last challenge set the size of the font withfontSizeinstead offont-size. Hyphenated words likefont-sizeare invalid syntax for JavaScript object properties, so React uses camel case. As a rule, any hyphenated style properties are written using camel case in JSX.", + "All property value length units (likeheight,width, andfontSize) are assumed to be inpxunless otherwise specified. If you want to useem, for example, you wrap the value and the units in quotes, like{fontSize: \"4em\"}. Other than the length values that default topx, all other property values should be wrapped in quotes.", + "
", + "If you have a large set of styles, you can assign a styleobjectto a constant to keep your code organized. Uncomment thestylesconstant and declare anobjectwith three style properties and their values. Give thediva color of\"purple\", a font-size of40, and a border of\"2px solid purple\". Then set thestyleattribute equal to thestylesconstant." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "", + "// const styles =", + "// change code above this line", + "class Colorful extends React.Component {", + " render() {", + " // change code below this line", + " return (", + "Style Me!", + " );", + " // change code above this line", + " }", + "};", + "" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "assert(Object.keys(styles).length === 3, 'message: The stylesvariable should be anobjectwith three properties.');", + "assert(styles.color === 'purple', 'message: Thestylesvariable should have acolorproperty set to a value ofpurple.');", + "assert(styles.fontSize === 40, 'message: Thestylesvariable should have afontSizeproperty set to a value of40.');", + "assert(styles.border === \"2px solid purple\", 'message: Thestylesvariable should have aborderproperty set to a value of2px solid purple.');", + "assert((function() { const mockedComponent = Enzyme.shallow(React.createElement(Colorful)); return mockedComponent.type() === 'div'; })(), 'message: The component should render adivelement.');", + "assert((function() { const mockedComponent = Enzyme.shallow(React.createElement(Colorful)); return (mockedComponent.props().style.color === \"purple\" && mockedComponent.props().style.fontSize === 40 && mockedComponent.props().style.border === \"2px solid purple\"); })(), 'message: Thedivelement should have its styles defined by thestylesobject.');" + ], + "solutions": [ + "\nconst styles = {\n color: \"purple\",\n fontSize: 40,\n border: \"2px solid purple\"\n};\n// change code above this line\nclass Colorful extends React.Component {\n render() {\n // change code below this line\n return (\nStyle Me!\n // change code above this line\n );\n }\n};\n" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036183", + "title": "Use Advanced JavaScript in React Render Method", + "releasedOn": "December 25, 2017", + "description": [ + "In previous challenges, you learned how to inject JavaScript code into JSX code using curly braces,{ }, for tasks like accessing props, passing props, accessing state, inserting comments into your code, and most recently, styling your components. These are all common use cases to put JavaScript in JSX, but they aren't the only way that you can utilize JavaScript code in your React components.", + "You can also write JavaScript directly in yourrendermethods, before thereturnstatement, without inserting it inside of curly braces. This is because it is not yet within the JSX code. When you want to use a variable later in the JSX code inside thereturnstatement, you place the variable name inside curly braces.", + "
", + "In the code provided, therendermethod has an array that contains 20 phrases to represent the answers found in the classic 1980's Magic Eight Ball toy. The button click event is bound to theaskmethod, so each time the button is clicked a random number will be generated and stored as therandomIndexin state. On line 38, delete the string\"change me!\"and reassign theanswerconst so your code randomly accesses a different index of thepossibleAnswersarray each time the component updates. Finally, insert theanswerconst inside theptags." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const inputStyle = {", + " width: 235,", + " margin: 5", + "}", + "", + "class MagicEightBall extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " userInput: '',", + " randomIndex: ''", + " }", + " this.ask = this.ask.bind(this);", + " this.handleChange = this.handleChange.bind(this);", + " }", + " ask() {", + " if (this.state.userInput) {", + " this.setState({", + " randomIndex: Math.floor(Math.random() * 20),", + " userInput: ''", + " });", + " }", + " }", + " handleChange(event) {", + " this.setState({", + " userInput: event.target.value", + " });", + " }", + " render() {", + " const possibleAnswers = [", + " 'It is certain',", + " 'It is decidedly so',", + " 'Without a doubt', ", + " 'Yes, definitely',", + " 'You may rely on it',", + " 'As I see it, yes',", + " 'Outlook good',", + " 'Yes',", + " 'Signs point to yes',", + " 'Reply hazy try again',", + " 'Ask again later',", + " 'Better not tell you now',", + " 'Cannot predict now',", + " 'Concentrate and ask again',", + " 'Don\\'t count on it', ", + " 'My reply is no',", + " 'My sources say no',", + " 'Most likely',", + " 'Outlook not so good',", + " 'Very doubtful'", + " ];", + " const answer = 'change me!' // << change code here", + " return (", + "", + "", + " );", + " }", + "};" + ], + "tail": [ + "var possibleAnswers = [ 'It is certain', 'It is decidedly so', 'Without a doubt', 'Yes, definitely', 'You may rely on it', 'As I see it, yes', 'Outlook good', 'Yes', 'Signs point to yes', 'Reply hazy try again', 'Ask again later', 'Better not tell you now', 'Cannot predict now', 'Concentrate and ask again', 'Don\\'t count on it', 'My reply is no', 'My sources say no', 'Outlook not so good','Very doubtful', 'Most likely' ];", + "ReactDOM.render(
", + "
", + "Answer:
", + "", + " { /* change code below this line */ }", + "", + " { /* change code above this line */ }", + "
", + ", document.getElementById('root'))" ] } }, - "backup-tests": [ - "(getUserInput) => assert(getUserInput('index').replace(/\\s/g, '').includes('classMyComponentextendsReact.Component{'), 'message: There should be a React component called MyComponent')" + "tests": [ + "assert.strictEqual(Enzyme.mount(React.createElement(MagicEightBall)).find('MagicEightBall').length, 1, 'message: The MagicEightBallcompponent should exist and should render to the page.');", + "assert.strictEqual(Enzyme.mount(React.createElement(MagicEightBall)).children().childAt(0).name(), 'input', \"message:MagicEightBall's first child should be aninputelement.\");", + "assert.strictEqual(Enzyme.mount(React.createElement(MagicEightBall)).children().childAt(2).name(), 'button', \"message:MagicEightBall's third child should be abuttonelement.\");", + "assert(Enzyme.mount(React.createElement(MagicEightBall)).state('randomIndex') === '' && Enzyme.mount(React.createElement(MagicEightBall)).state('userInput') === '', \"message:MagicEightBall's state should be initialized with a property ofuserInputand a property ofrandomIndexboth set to a value of an empty string.\");", + "assert(Enzyme.mount(React.createElement(MagicEightBall)).find('p').length === 1 && Enzyme.mount(React.createElement(MagicEightBall)).find('p').text() === '', 'message: WhenMagicEightBallis first mounted to the DOM, it should return an emptypelement.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const comp = Enzyme.mount(React.createElement(MagicEightBall)); const simulate = () => { comp.find('input').simulate('change', { target: { value: 'test?' }}); comp.find('button').simulate('click'); }; const result = () => comp.find('p').text(); const _1 = () => { simulate(); return waitForIt(() => result()) }; const _2 = () => { simulate(); return waitForIt(() => result()) }; const _3 = () => { simulate(); return waitForIt(() => result()) }; const _4 = () => { simulate(); return waitForIt(() => result()) }; const _5 = () => { simulate(); return waitForIt(() => result()) }; const _6 = () => { simulate(); return waitForIt(() => result()) }; const _7 = () => { simulate(); return waitForIt(() => result()) }; const _8 = () => { simulate(); return waitForIt(() => result()) }; const _9 = () => { simulate(); return waitForIt(() => result()) }; const _10 = () => { simulate(); return waitForIt(() => result()) }; const _1_val = await _1(); const _2_val = await _2(); const _3_val = await _3(); const _4_val = await _4(); const _5_val = await _5(); const _6_val = await _6(); const _7_val = await _7(); const _8_val = await _8(); const _9_val = await _9(); const _10_val = await _10(); const actualAnswers = [_1_val, _2_val, _3_val, _4_val, _5_val, _6_val, _7_val, _8_val, _9_val, _10_val]; const hasIndex = actualAnswers.filter((answer, i) => possibleAnswers.indexOf(answer) !== -1); const notAllEqual = new Set(actualAnswers); assert(notAllEqual.size > 1 && hasIndex.length === 10, 'message: When text is entered into theinputelement and the button is clicked, theMagicEightBallcompponent should return apelement that contains a random element from thepossibleAnswersarray.'); }; " ], - "head": [], - "tail": [], "solutions": [ - "// change code below this line\nclass MyComponent extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\n\n\n );\n }\n};\n\nReactDOM.render(My First React Component!
\n, document.getElementById('challenge-node'));" + "\nconst inputStyle = {\n width: 235,\n margin: 5\n}\n\nclass MagicEightBall extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n userInput: '',\n randomIndex: ''\n }\n this.ask = this.ask.bind(this);\n this.handleChange = this.handleChange.bind(this);\n }\n ask() {\n if (this.state.userInput) {\n this.setState({\n randomIndex: Math.floor(Math.random() * 20),\n userInput: ''\n });\n }\n }\n handleChange(event) {\n this.setState({\n userInput: event.target.value\n });\n }\n render() {\n const possibleAnswers = [\n \"It is certain\", \"It is decidedly so\", \"Without a doubt\",\n \"Yes, definitely\", \"You may rely on it\", \"As I see it, yes\",\n \"Outlook good\", \"Yes\", \"Signs point to yes\", \"Reply hazy try again\",\n \"Ask again later\", \"Better not tell you now\", \"Cannot predict now\",\n \"Concentrate and ask again\", \"Don't count on it\", \"My reply is no\",\n \"My sources say no\", \"Outlook not so good\",\"Very doubtful\", \"Most likely\"\n ];\n const answer = possibleAnswers[this.state.randomIndex];\n return (\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036184", + "title": "Render with an If/Else Condition", + "releasedOn": "December 25, 2017", + "description": [ + "Another application of using JavaScript to control your rendered view is to tie the elements that are rendered to a condition. When the condition is true, one view renders, when it's false, it's a different view. You can do this with a standard
\n
\nAnswer:
\n\n {answer}\n
\nif/elsestatement in therender()method of a React component.", + "
", + "MyComponent contains abooleanin its state which tracks whether you want to display some element in the UI or not. Thebuttontoggles the state of this value. Currently, it renders the same UI every time. Rewrite therender()method with anif/elsestatement so that ifdisplayistrue, you return the current markup. Otherwise, return the markup without theh1element.", + "Note: You must write anif/elseto pass the tests, use of the ternary operator will not pass here." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " display: true", + " }", + " this.toggleDisplay = this.toggleDisplay.bind(this);", + " }", + " toggleDisplay() {", + " this.setState({", + " display: !this.state.display", + " });", + " }", + " render() {", + " // change code below this line", + "", + " return (", + "", + " ", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Displayed!
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.find('MyComponent').length === 1; })(), 'message: MyComponentshould exist and render.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const state_1 = () => { mockedComponent.setState({display: true}); return waitForIt(() => mockedComponent )}; const updated = await state_1(); assert(mockedComponent.find('div').length === 1 && mockedComponent.find('div').children().length === 2 && mockedComponent.find('button').length === 1 && mockedComponent.find('h1').length === 1, 'message: Whendisplayis set totrue, adiv,button, andh1should render.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const state_1 = () => { mockedComponent.setState({display: false}); return waitForIt(() => mockedComponent )}; const updated = await state_1(); assert(mockedComponent.find('div').length === 1 && mockedComponent.find('div').children().length === 1 && mockedComponent.find('button').length === 1 && mockedComponent.find('h1').length === 0, 'message: Whendisplayis set tofalse, only adivandbuttonshould render.'); }; ", + "getUserInput => assert(getUserInput('index').includes('if') === true && getUserInput('index').includes('else') === true, 'message: The render method should use anif/elsestatement to check the condition ofthis.state.display.');" + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n display: true\n }\n this.toggleDisplay = this.toggleDisplay.bind(this); \n }\n toggleDisplay() {\n this.setState({\n display: !this.state.display\n });\n }\n render() {\n // change code below this line\n if (this.state.display) {\n return (\n\n \n\n );\n } else {\n return (\nDisplayed!
\n\n \n\n );\n }\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036185", + "title": "Use && for a More Concise Conditional", + "releasedOn": "December 25, 2017", + "description": [ + "The if/else statements worked in the last challenge, but there's a more concise way to achieve the same result. Imagine that you are tracking several conditions in a component and you want different elements to render depending on each of these conditions. If you write a lot ofelse ifstatements to return slightly different UIs, you may repeat code which leaves room for error. Instead, you can use the&&logical operator to perform conditional logic in a more concise way. This is possible because you want to check if a condition istrue, and if it is, return some markup. Here's an example:", + "{condition && <p>markup</p>}", + "If theconditionistrue, the markup will be returned. If the condition isfalse, the operation will immediately returnfalseafter evaluating theconditionand return nothing. You can include these statements directly in your JSX and string multiple conditions together by writing&&after each one. This allows you to handle more complex conditional logic in yourrender()method without repeating a lot of code.", + "
", + "Solve the previous example again, so theh1only renders ifdisplayistrue, but use the&&logical operator instead of anif/elsestatement." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " display: true", + " }", + " this.toggleDisplay = this.toggleDisplay.bind(this);", + " }", + " toggleDisplay() {", + " this.setState({", + " display: !this.state.display", + " });", + " }", + " render() {", + " // change code below this line", + " return (", + "", + " ", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Displayed!
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); return mockedComponent.find('MyComponent').length; })(), 'message: MyComponentshould exist and render.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const state_1 = () => { mockedComponent.setState({display: true}); return waitForIt(() => mockedComponent )}; const updated = await state_1(); assert(updated.find('div').length === 1 && updated.find('div').children().length === 2 && updated.find('button').length === 1 && updated.find('h1').length === 1, 'message: Whendisplayis set totrue, adiv,button, andh1should render.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(MyComponent)); const state_1 = () => { mockedComponent.setState({display: false}); return waitForIt(() => mockedComponent )}; const updated = await state_1(); assert(updated.find('div').length === 1 && updated.find('div').children().length === 1 && updated.find('button').length === 1 && updated.find('h1').length === 0, 'message: Whendisplayis set tofalse, only adivandbuttonshould render.'); }; ", + "getUserInput=> assert(getUserInput('index').includes('&&'), 'message: The render method should use the && logical operator to check the condition of this.state.display.');" + ], + "solutions": [ + "class MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n display: true\n }\n this.toggleDisplay = this.toggleDisplay.bind(this); \n }\n toggleDisplay() {\n this.setState({\n display: !this.state.display\n });\n }\n render() {\n // change code below this line\n return (\n\n \n {this.state.display &&\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036187", + "title": "Use a Ternary Expression for Conditional Rendering", + "releasedOn": "December 25, 2017", + "description": [ + "Before moving on to dynamic rendering techniques, there's one last way to use built-in JavaScript conditionals to render what you want: the ternary operator. The ternary operator is often utilized as a shortcut forDisplayed!
}\nif/elsestatements in JavaScript. They're not quite as robust as traditionalif/elsestatements, but they are very popular among React developers. One reason for this is because of how JSX is compiled,if/elsestatements can't be inserted directly into JSX code. You might have noticed this a couple challenges ago — when anif/elsestatement was required, it was always outside thereturnstatement. Ternary expressions can be an excellent alternative if you want to implement conditional logic within your JSX. Recall that a ternary operator has three parts, but you can combine several ternary expressions together. Here's the basic syntax:", + "condition ? expressionIfTrue : expressionIfFalse", + "
", + "The code editor has three constants defined within theCheckUserAgecomponent'srender()method. They are calledbuttonOne,buttonTwo, andbuttonThree. Each of these is assigned a simple JSX expression representing a button element. First, initialize the state ofCheckUserAgewithinputanduserAgeboth set to values of an empty string.", + "Once the component is rendering information to the page, users should have a way to interact with it. Within the component'sreturnstatement, set up a ternary expression that implements the following logic: when the page first loads, render the submit button,buttonOne, to the page. Then, when a user enters their age and clicks the button, render a different button based on the age. If a user enters a number less than18, renderbuttonThree. If a user enters a number greater than or equal to18, renderbuttonTwo." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "", + "const inputStyle = {", + " width: 235,", + " margin: 5", + "}", + "", + "class CheckUserAge extends React.Component {", + " constructor(props) {", + " super(props);", + " // change code below this line", + "", + " // change code above this line", + " this.submit = this.submit.bind(this);", + " this.handleChange = this.handleChange.bind(this);", + " }", + " handleChange(e) {", + " this.setState({", + " input: e.target.value,", + " userAge: ''", + " });", + " }", + " submit() {", + " this.setState({", + " userAge: this.state.input", + " });", + " }", + " render() {", + " const buttonOne = ;", + " const buttonTwo = ;", + " const buttonThree = ;", + " return (", + "", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Enter Your Age to Continue
", + "
", + " {", + " /* change code here */", + " }", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert(Enzyme.mount(React.createElement(CheckUserAge)).find('div').find('input').length === 1 && Enzyme.mount(React.createElement(CheckUserAge)).find('div').find('button').length === 1, 'message: The CheckUserAgecomponent should render with a singleinputelement and a singlebuttonelement.');", + "assert(Enzyme.mount(React.createElement(CheckUserAge)).state().input === '' && Enzyme.mount(React.createElement(CheckUserAge)).state().userAge === '', \"message: TheCheckUserAgecomponent's state should be initialized with a property ofuserAgeand a property ofinput, both set to a value of an empty string.\");", + "assert(Enzyme.mount(React.createElement(CheckUserAge)).find('button').text() === 'Submit', \"message: When theCheckUserAgecomponent is first rendered to the DOM, thebutton's inner text should be Submit.\");", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(CheckUserAge)); const initialButton = mockedComponent.find('button').text(); const enter3AndClickButton = () => { mockedComponent.find('input').simulate('change', {target: { value: 3 }}); mockedComponent.find('button').simulate('click'); return waitForIt(() => { mockedComponent.update(); return mockedComponent.find('button').text(); }); }; const enter17AndClickButton = () => { mockedComponent.find('input').simulate('change', {target: { value: 17 }}); mockedComponent.find('button').simulate('click'); return waitForIt(() => { mockedComponent.update(); return mockedComponent.find('button').text(); }); }; const userAge3 = await enter3AndClickButton(); const userAge17 = await enter17AndClickButton(); assert(initialButton === 'Submit' && userAge3 === 'You Shall Not Pass' && userAge17 === 'You Shall Not Pass', 'message: When a number of less than 18 is entered into theinputelement and thebuttonis clicked, thebutton's inner text should readYou Shall Not Pass.); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(CheckUserAge)); const initialButton = mockedComponent.find('button').text(); const enter18AndClickButton = () => { mockedComponent.find('input').simulate('change', {target: { value: 18 }}); mockedComponent.find('button').simulate('click'); return waitForIt(() => { mockedComponent.update(); return mockedComponent.find('button').text(); }); }; const enter35AndClickButton = () => { mockedComponent.find('input').simulate('change', {target: { value: 35 }}); mockedComponent.find('button').simulate('click'); return waitForIt(() => { mockedComponent.update(); return mockedComponent.find('button').text(); }); }; const userAge18 = await enter18AndClickButton(); const userAge35 = await enter35AndClickButton(); assert(initialButton === 'Submit' && userAge18 === 'You May Enter' && userAge35 === 'You May Enter', 'message: When a number greater than or equal to 18 is entered into theinputelement and thebuttonis clicked, thebutton's inner text should readYou May Enter.); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const mockedComponent = Enzyme.mount(React.createElement(CheckUserAge)); const enter18AndClickButton = () => { mockedComponent.find('input').simulate('change', {target: { value: 18 }}); mockedComponent.find('button').simulate('click'); return waitForIt(() => { mockedComponent.update(); return mockedComponent.find('button').text(); }); }; const changeInputDontClickButton = () => { mockedComponent.find('input').simulate('change', {target: { value: 5 }}); return waitForIt(() => { mockedComponent.update(); return mockedComponent.find('button').text(); }); }; const enter10AndClickButton = () => { mockedComponent.find('input').simulate('change', {target: { value: 10 }}); mockedComponent.find('button').simulate('click'); return waitForIt(() => { mockedComponent.update(); return mockedComponent.find('button').text(); }); }; const userAge18 = await enter18AndClickButton(); const changeInput1 = await changeInputDontClickButton(); const userAge10 = await enter10AndClickButton(); const changeInput2 = await changeInputDontClickButton(); assert(userAge18 === 'You May Enter' && changeInput1 === 'Submit' && userAge10 === 'You Shall Not Pass' && changeInput2 === 'Submit', 'message: Once a number has been submitted, and the value of theinputis once again changed, thebuttonshould return to readingSubmit.'); }; ", + "assert(new RegExp(/(\\s|;)if(\\s|\\()/).test(Enzyme.mount(React.createElement(CheckUserAge)).instance().render.toString()) === false, 'message: Your code should not contain anyif/elsestatements.');" + ], + "solutions": [ + "\nconst inputStyle = {\n width: 235,\n margin: 5\n}\n\nclass CheckUserAge extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n userAge: '',\n input: ''\n }\n this.submit = this.submit.bind(this);\n this.handleChange = this.handleChange.bind(this);\n }\n handleChange(e) {\n this.setState({\n input: e.target.value,\n userAge: ''\n });\n }\n submit() {\n this.setState({\n userAge: this.state.input\n });\n }\n render() {\n const buttonOne = ;\n const buttonTwo = ;\n const buttonThree = ;\n return (\n\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036188", + "title": "Render Conditionally from Props", + "releasedOn": "December 25, 2017", + "description": [ + "So far, you've seen how to useEnter Your Age to Continue
\n
\n {\n this.state.userAge === '' ?\n buttonOne :\n this.state.userAge >= 18 ?\n buttonTwo :\n buttonThree\n }\nif/else,&&,nulland the ternary operator (condition ? expressionIfTrue : expressionIfFalse) to make conditional decisions about what to render and when. However, there's one important topic left to discuss that lets you combine any or all of these concepts with another powerful React feature: props. Using props to conditionally render code is very common with React developers — that is, they use the value of a given prop to automatically make decisions about what to render.", + "In this challenge, you'll set up a child component to make rendering decisions based on props. You'll also use the ternary operator, but you can see how several of the other concepts that were covered in the last few challenges might be just as useful in this context.", + "
", + "The code editor has two components that are partially defined for you: a parent calledGameOfChance, and a child calledResults. They are used to create a simple game where the user presses a button to see if they win or lose.", + "First, you'll need a simple expression that randomly returns a different value every time it is run. You can useMath.random(). This method returns a value between0(inclusive) and1(exclusive) each time it is called. So for 50/50 odds, useMath.random() > .5in your expression. Statistically speaking, this expression will returntrue50% of the time, andfalsethe other 50%. On line 30, replace the comment with this expression to complete the variable declaration.", + "Now you have an expression that you can use to make a randomized decision in the code. Next you need to implement this. Render theResultscomponent as a child ofGameOfChance, and pass inexpressionas a prop calledfiftyFifty. In theResultscomponent, write a ternary expression to render the text\"You win!\"or\"You lose!\"based on thefiftyFiftyprop that's being passed in fromGameOfChance. Finally, make sure thehandleClick()method is correctly counting each turn so the user knows how many times they've played. This also serves to let the user know the component has actually updated in case they win or lose twice in a row." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class Results extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " render() {", + " return (", + "", + " {", + " /* change code here */", + " }", + "
", + " )", + " };", + "};", + "", + "class GameOfChance extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " counter: 1", + " }", + " this.handleClick = this.handleClick.bind(this);", + " }", + " handleClick() {", + " this.setState({", + " counter: // change code here", + " });", + " }", + " render() {", + " let expression = // change code here", + " return (", + "", + " ", + " { /* change code below this line */ }", + "", + " { /* change code above this line */ }", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render({'Turn: ' + this.state.counter}
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert.strictEqual(Enzyme.mount(React.createElement(GameOfChance)).find('GameOfChance').length, 1, 'message: The GameOfChancecomponent should exist and render to the page.');", + "assert.strictEqual(Enzyme.mount(React.createElement(GameOfChance)).find('button').length, 1, 'message:GameOfChanceshould return a singlebuttonelement.');", + "assert(Enzyme.mount(React.createElement(GameOfChance)).find('Results').length === 1 && Enzyme.mount(React.createElement(GameOfChance)).find('Results').props().hasOwnProperty('fiftyFifty') === true, 'message:GameOfChanceshould return a single instance of theResultscomponent, which has a prop calledfiftyFifty.');", + "assert.strictEqual(Enzyme.mount(React.createElement(GameOfChance)).state().counter, 1, 'message:GameOfChancestate should be initialized with a property ofcounterset to a value of1.');", + "assert.strictEqual(Enzyme.mount(React.createElement(GameOfChance)).find('p').text(), 'Turn: 1', 'message: When theGameOfChancecomponent is first rendered to the DOM, apelement should be returned with the inner text ofTurn: 1.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const comp = Enzyme.mount(React.createElement(GameOfChance)); const simulate = () => { comp.find('button').simulate('click'); };const result = () => ({ count: comp.state('counter'), text: comp.find('p').text() });const _1 = () => { simulate(); return waitForIt(() => result())}; const _2 = () => { simulate(); return waitForIt(() => result())}; const _3 = () => { simulate(); return waitForIt(() => result())}; const _4 = () => { simulate(); return waitForIt(() => result())}; const _5 = () => { simulate(); return waitForIt(() => result())}; const _1_val = await _1(); const _2_val = await _2(); const _3_val = await _3(); const _4_val = await _4(); const _5_val = await _5(); assert(_1_val.count === 2 && _1_val.text === 'Turn: 2' && _2_val.count === 3 && _2_val.text === 'Turn: 3' && _3_val.count === 4 && _3_val.text === 'Turn: 4' && _4_val.count === 5 && _4_val.text === 'Turn: 5' && _5_val.count === 6 && _5_val.text === 'Turn: 6', 'message: Each time the buton is clicked, the counter state should be incremented by a value of 1, and a singlepelement should be rendered to the DOM that contains the text 'Turn: N', where N is the value of the counter state.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const comp = Enzyme.mount(React.createElement(GameOfChance)); const simulate = () => { comp.find('button').simulate('click'); };const result = () => ({ h1: comp.find('h1').length, text: comp.find('h1').text() });const _1 = result(); const _2 = () => { simulate(); return waitForIt(() => result())}; const _3 = () => { simulate(); return waitForIt(() => result())}; const _4 = () => { simulate(); return waitForIt(() => result())}; const _5 = () => { simulate(); return waitForIt(() => result())}; const _6 = () => { simulate(); return waitForIt(() => result())}; const _7 = () => { simulate(); return waitForIt(() => result())}; const _8 = () => { simulate(); return waitForIt(() => result())}; const _9 = () => { simulate(); return waitForIt(() => result())}; const _10 = () => { simulate(); return waitForIt(() => result())}; const _2_val = await _2(); const _3_val = await _3(); const _4_val = await _4(); const _5_val = await _5(); const _6_val = await _6(); const _7_val = await _7(); const _8_val = await _8(); const _9_val = await _9(); const _10_val = await _10(); const __text = new Set([_1.text, _2_val.text, _3_val.text, _4_val.text, _5_val.text, _6_val.text, _7_val.text, _8_val.text, _9_val.text, _10_val.text]); const __h1 = new Set([_1.h1, _2_val.h1, _3_val.h1, _4_val.h1, _5_val.h1, _6_val.h1, _7_val.h1, _8_val.h1, _9_val.h1, _10_val.h1]); assert(__text.size === 2 && __h1.size === 1, 'message: When theGameOfChancecomponent is first mounted to the DOM and each time the button is clicked thereafter, a singleh1element should be returned that randomly renders eitherYou Win!orYou Lose!.'); }; " + ], + "solutions": [ + "class Results extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return (\n\n {\n this.props.fiftyFifty ?\n 'You Win!' :\n 'You Lose!'\n }\n
\n )\n };\n};\n\nclass GameOfChance extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n counter: 1\n }\n this.handleClick = this.handleClick.bind(this);\n }\n handleClick() {\n this.setState({\n counter: this.state.counter + 1\n });\n }\n render() {\n const expression = Math.random() > .5;\n return (\n\n \n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d4036189", + "title": "Change Inline CSS Conditionally Based on Component State", + "releasedOn": "December 25, 2017", + "description": [ + "At this point, you've seen several applications of conditional rendering and the use of inline styles. Here's one more example that combines both of these topics. You can also render CSS conditionally based on the state of a React component. To do this, you check for a condition, and if that condition is met, you modify the styles object that's assigned to the JSX elements in the render method.", + "This paradigm is important to understand because it is a dramatic shift from the more traditional approach of applying styles by modifying DOM elements directly (which is very common with jQuery, for example). In that approach, you must keep track of when elements change and also handle the actual manipulation directly. It can become difficult to keep track of changes, potentially making your UI unpredictable. When you set a style object based on a condition, you describe how the UI should look as a function of the application's state. There is a clear flow of information that only moves in one direction. This is the preferred method when writing applications with React.", + "\n {'Turn: ' + this.state.counter}
\n
", + "The code editor has a simple controlled input component with a styled border. You want to style this border red if the user types more than 15 characters of text in the input box. Add a condition to check for this and, if the condition is valid, set the input border style to3px solid red. You can try it out by entering text in the input." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "", + "class GateKeeper extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " input: ''", + " };", + " this.handleChange = this.handleChange.bind(this);", + " }", + " handleChange(event) {", + " this.setState({ input: event.target.value })", + " }", + " render() {", + " let inputStyle = {", + " border: '1px solid black'", + " };", + " // change code below this line", + "", + " // change code above this line", + " return (", + "", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Don't Type Too Much:
", + " ", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(GateKeeper)); return mockedComponent.find('div').length === 1; })(), 'message: The GateKeepercomponent should render adivelement.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(GateKeeper)); return mockedComponent.state().input === ''; })(), 'message: TheGateKeepercomponent should be initialized with a state keyinputset to an empty string.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(GateKeeper)); return mockedComponent.find('h3').length === 1 && mockedComponent.find('input').length === 1; })(), 'message: TheGateKeepercomponent should render anh3tag and aninputtag.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(GateKeeper)); return mockedComponent.find('input').props().style.border === '1px solid black'; })(), 'message: Theinputtag should initially have a style of1px solid blackfor theborderproperty.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); const mockedComponent = Enzyme.mount(React.createElement(GateKeeper)); const simulateChange = (el, value) => el.simulate('change', {target: {value}}); let initialStyle = mockedComponent.find('input').props().style.border; const state_1 = () => { mockedComponent.setState({input: 'this is 15 char' }); return waitForIt(() => mockedComponent.find('input').props().style.border )}; const state_2 = () => { mockedComponent.setState({input: 'A very long string longer than 15 characters.' }); return waitForIt(() => mockedComponent.find('input').props().style.border )}; const style_1 = await state_1(); const style_2 = await state_2(); assert(initialStyle === '1px solid black' && style_1 === '1px solid black' && style_2 === '3px solid red', 'message: Theinputtag should be styled with a border of3px solid redif the input value in state is longer than 15 characters.'); }; " + ], + "solutions": [ + "\nclass GateKeeper extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n input: ''\n };\n this.handleChange = this.handleChange.bind(this);\n }\n handleChange(event) {\n this.setState({ input: event.target.value })\n }\n render() {\n let inputStyle = {\n border: '1px solid black'\n };\n if (this.state.input.length > 15) {\n inputStyle.border = '3px solid red';\n };\n return (\n\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403618a", + "title": "Use Array.map() to Dynamically Render Elements", + "releasedOn": "December 25, 2017", + "description": [ + "Conditional rendering is useful, but you may need your components to render an unknown number of elements. Often in reactive programming, a programmer has no way to know what the state of an application is until runtime, because so much depends on a user's interaction with that program. Programmers need to write their code to correctly handle that unknown state ahead of time. UsingDon't Type Too Much:
\n \nArray.map()in React illustrates this concept.", + "For example, you create a simple \"To Do List\" app. As the programmer, you have no way of knowing how many items a user might have on their list. You need to set up your component to dynamically render the correct number of list elements long before someone using the program decides that today is laundry day. ", + "
", + "The code editor has most of theMyToDoListcomponent set up. Some of this code should look familiar if you completed the controlled form challenge. You'll notice atextareaand abutton, along with a couple of methods that track their states, but nothing is rendered to the page yet.", + "Inside theconstructor, create athis.stateobject and define two states:userInputshould be initialized as an empty string, andtoDoListshould be initialized as an empty array. Next, delete the comment in therender()method next to theitemsvariable. In its place, map over thetoDoListarray stored in the component's internal state and dynamically render alifor each item. Try entering the stringeat, code, sleep, repeatinto thetextarea, then click the button and see what happens.", + "Note: You may know that all sibling child elements created by a mapping operation like this do need to be supplied with a uniquekeyattribute. Don't worry, this is the topic of the next challenge." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const textAreaStyles = {", + " width: 235,", + " margin: 5", + "};", + "", + "class MyToDoList extends React.Component {", + " constructor(props) {", + " super(props);", + " // change code below this line", + "", + " // change code above this line", + " this.handleSubmit = this.handleSubmit.bind(this);", + " this.handleChange = this.handleChange.bind(this);", + " }", + " handleSubmit() {", + " const itemsArray = this.state.userInput.split(',');", + " this.setState({", + " toDoList: itemsArray", + " });", + " }", + " handleChange(e) {", + " this.setState({", + " userInput: e.target.value", + " });", + " }", + " render() {", + " const items = // change code here", + " return (", + "", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(
", + " ", + "My \"To Do\" List:
", + "", + " {items}", + "
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyToDoList)); return mockedComponent.find('MyToDoList').length === 1; })(), 'message: The MyToDoList component should exist and render to the page.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyToDoList)); return mockedComponent.find('MyToDoList').children().childAt(0).type() === 'textarea'; })(), 'message: The first child of MyToDoListshould be atextareaelement.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyToDoList)); return mockedComponent.find('MyToDoList').children().childAt(2).type() === 'button'; })(), 'message: The third child ofMyToDoListshould be abuttonelement.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyToDoList)); const initialState = mockedComponent.state(); return Array.isArray(initialState.toDoList) === true && initialState.toDoList.length === 0; })(), 'message: The state ofMyToDoListshould be initialized withtoDoListas an empty array.');", + "assert((function() { const mockedComponent = Enzyme.mount(React.createElement(MyToDoList)); const initialState = mockedComponent.state(); return typeof initialState.userInput === 'string' && initialState.userInput.length === 0; })(), 'message: The state ofMyToDoListshould be initialized withuserInputas an empty string.');", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 100)); const mockedComponent = Enzyme.mount(React.createElement(MyToDoList)); const simulateChange = (el, value) => el.simulate('change', {target: {value}}); const state_1 = () => { return waitForIt(() => mockedComponent.find('ul').find('li'))}; const setInput = () => { return waitForIt(() => simulateChange(mockedComponent.find('textarea'), \"testA, testB, testC\"))}; const click = () => { return waitForIt(() => mockedComponent.find('button').simulate('click'))}; const state_2 = () => { return waitForIt(() => { const nodes = mockedComponent.find('ul').find('li'); return { nodes, text: nodes.reduce((t, n) => t + n.text(), '') }; })}; const setInput_2 = () => { return waitForIt(() => simulateChange(mockedComponent.find('textarea'), \"t1, t2, t3, t4, t5, t6\"))}; const click_1 = () => { return waitForIt(() => mockedComponent.find('button').simulate('click'))}; const state_3 = () => { return waitForIt(() => { const nodes = mockedComponent.find('ul').find('li'); return { nodes, text: nodes.reduce((t, n) => t + n.text(), '') }; })}; const awaited_state_1 = await state_1(); const awaited_setInput = await setInput(); const awaited_click = await click(); const awaited_state_2 = await state_2(); const awaited_setInput_2 = await setInput_2(); const awaited_click_1 = await click_1(); const awaited_state_3 = await state_3(); assert(awaited_state_1.length === 0 && awaited_state_2.nodes.length === 3 && awaited_state_3.nodes.length === 6 && awaited_state_2.text === 'testA testB testC' && awaited_state_3.text === 't1 t2 t3 t4 t5 t6', 'message: When theCreate Listbutton is clicked, theMyToDoListcomponent should dynamically return an unordered list that contains a list item element for every item of a comma-separated list entered into thetextareaelement.'); }; " + ], + "solutions": [ + "\nconst textAreaStyles = {\n width: 235,\n margin: 5\n};\n\nclass MyToDoList extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n toDoList: [],\n userInput: ''\n }\n this.handleSubmit = this.handleSubmit.bind(this);\n this.handleChange = this.handleChange.bind(this);\n }\n handleSubmit() {\n const itemsArray = this.state.userInput.split(',');\n this.setState({\n toDoList: itemsArray\n });\n }\n handleChange(e) {\n this.setState({\n userInput: e.target.value\n });\n }\n render() {\n const items = this.state.toDoList.map( (item, i) => {\n return{item} \n });\n return (\n\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403618b", + "title": "Give Sibling Elements a Unique Key Attribute", + "releasedOn": "December 25, 2017", + "description": [ + "The last challenge showed how the
\n \nMy \"To Do\" List:
\n\n {items}\n
\nmapmethod is used to dynamically render a number of elements based on user input. However, there was an important piece missing from that example. When you create an array of elements, each one needs akeyattribute set to a unique value. React uses these keys to keep track of which items are added, changed, or removed. This helps make the re-rendering process more efficient when the list is modified in any way. Note that keys only need to be unique between sibling elements, they don't need to be globally unique in your application.", + "
", + "The code editor has an array with some front end frameworks and a stateless functional component namedFrameworks().Frameworks()needs to map the array to an unordered list, much like in the last challenge. Finish writing themapcallback to return anlielement for each framework in thefrontEndFrameworksarray. This time, make sure to give eachliakeyattribute, set to a unique value.", + "Normally, you want to make the key something that uniquely identifies the element being rendered. As a last resort the array index may be used, but typically you should try to use a unique identification." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "", + "const frontEndFrameworks = [", + " 'React',", + " 'Angular',", + " 'Ember',", + " 'Knockout',", + " 'Backbone',", + " 'Vue'", + "];", + "", + "function Frameworks() {", + " const renderFrameworks = // change code here", + " return (", + "", + "", + " );", + "};" + ], + "tail": "ReactDOM.render(Popular Front End JavaScript Frameworks
", + "", + " {renderFrameworks}", + "
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert(Enzyme.mount(React.createElement(Frameworks)).find('Frameworks').length === 1, 'message: The Frameworkscomponent should exist and render to the page.');", + "assert(Enzyme.mount(React.createElement(Frameworks)).find('h1').length === 1, 'message:Frameworksshould render anh1element.');", + "assert(Enzyme.mount(React.createElement(Frameworks)).find('ul').length === 1, 'message:Frameworksshould render aulelement.');", + "assert(Enzyme.mount(React.createElement(Frameworks)).find('ul').children().length === 6 && Enzyme.mount(React.createElement(Frameworks)).find('ul').childAt(0).name() === 'li' && Enzyme.mount(React.createElement(Frameworks)).find('li').length === 6, 'message: Theultag should render 6 childlielements.');", + "assert((() => { const ul = Enzyme.mount(React.createElement(Frameworks)).find('ul'); const keys = new Set([ ul.childAt(0).key(), ul.childAt(1).key(), ul.childAt(2).key(), ul.childAt(3).key(), ul.childAt(4).key(), ul.childAt(5).key(), ]); return keys.size === 6; })(), 'message: Each list item element should have a uniquekeyattribute.');" + ], + "solutions": [ + "\nconst frontEndFrameworks = [\n 'React',\n 'Angular',\n 'Ember',\n 'Knockout',\n 'Backbone',\n 'Vue'\n];\n\nfunction Frameworks() {\n const renderFrameworks = frontEndFrameworks.map((fw, i) => {\n return{fw} \n })\n return (\n\n\n );\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403618c", + "title": "Use Array.filter() to Dynamically Filter an Array", + "releasedOn": "December 25, 2017", + "description": [ + "ThePopular Front End JavaScript Frameworks
\n\n {renderFrameworks}\n
\nmaparray method is a powerful tool that you will use often when working with React. Another method related tomapisfilter, which filters the contents of an array based on a condition, then returns a new array. For example, if you have an array of users that all have a propertyonlinewhich can be set totrueorfalse, you can filter only those users that are online by writing:", + "let onlineUsers = users.filter(user => user.online);", + "
", + "In the code editor,MyComponent'sstateis initialized with an array of users. Some users are online and some aren't. Filter the array so you see only the users who are online. To do this, first usefilterto return a new array containing only the users whoseonlineproperty istrue. Then, in therenderOnlinevariable, map over the filtered array, and return alielement for each user that contains the text of theirusername. Be sure to include a uniquekeyas well, like in the last challenges." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "class MyComponent extends React.Component {", + " constructor(props) {", + " super(props);", + " this.state = {", + " users: [", + " {", + " username: 'Jeff',", + " online: true", + " },", + " {", + " username: 'Alan',", + " online: false", + " },", + " {", + " username: 'Mary',", + " online: true", + " },", + " {", + " username: 'Jim',", + " online: false", + " },", + " {", + " username: 'Sara',", + " online: true", + " },", + " {", + " username: 'Laura',", + " online: true", + " }", + " ]", + " }", + " }", + " render() {", + " const usersOnline = // change code here", + " const renderOnline = // change code here", + " return (", + "", + "", + " );", + " }", + "};" + ], + "tail": "ReactDOM.render(Current Online Users:
", + "", + " {renderOnline}", + "
", + ", document.getElementById('root'))" + } + }, + "tests": [ + "assert.strictEqual(Enzyme.mount(React.createElement(MyComponent)).find('MyComponent').length, 1, 'message: MyComponentshould exist and render to the page.');", + "assert(Array.isArray(Enzyme.mount(React.createElement(MyComponent)).state('users')) === true && Enzyme.mount(React.createElement(MyComponent)).state('users').length === 6, \"message:MyComponent's state should be initialized to an array of six users.\");", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const comp = Enzyme.mount(React.createElement(MyComponent)); const users = (bool) => ({users:[ { username: 'Jeff', online: bool }, { username: 'Alan', online: bool }, { username: 'Mary', online: bool }, { username: 'Jim', online: bool }, { username: 'Laura', online: bool } ]}); const result = () => comp.find('li').length; const _1 = result(); const _2 = () => { comp.setState(users(true)); return waitForIt(() => result()) }; const _3 = () => { comp.setState(users(false)); return waitForIt(() => result()) }; const _4 = () => { comp.setState({ users: [] }); return waitForIt(() => result()) }; const _2_val = await _2(); const _3_val = await _3(); const _4_val = await _4(); assert(comp.find('div').length === 1 && comp.find('h1').length === 1 && comp.find('ul').length === 1 && _1 === 4 && _2_val === 5 && _3_val === 0 && _4_val === 0, 'message:MyComponentshould return adiv, anh1, and then an unordered list containinglielements for every user whose online status is set totrue.'); }; ", + "async () => { const waitForIt = (fn) => new Promise((resolve, reject) => setTimeout(() => resolve(fn()), 250)); const comp = Enzyme.mount(React.createElement(MyComponent)); const users = (bool) => ({users:[ { username: 'Jeff', online: bool }, { username: 'Alan', online: bool }, { username: 'Mary', online: bool }, { username: 'Jim', online: bool }, { username: 'Laura', online: bool } ]}); const ul = () => { comp.setState(users(true)); return waitForIt(() => comp.find('ul').html()) }; const html = await ul(); assert(html === ', 'message:
- Jeff
- Alan
- Mary
- Jim
- Laura
MyComponentshould renderlielements that contain the username of each online user.); }; ", + "assert((() => { const ul = Enzyme.mount(React.createElement(MyComponent)).find('ul'); console.log(ul.debug()); const keys = new Set([ ul.childAt(0).key(), ul.childAt(1).key(), ul.childAt(2).key(), ul.childAt(3).key() ]); return keys.size === 4; })(), 'message: Each list item element should have a uniquekeyattribute.');" + ], + "solutions": [ + "\nclass MyComponent extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n users: [\n {\n username: 'Jeff',\n online: true\n },\n {\n username: 'Alan',\n online: false\n },\n {\n username: 'Mary',\n online: true\n },\n {\n username: 'Jim',\n online: false\n },\n {\n username: 'Sara',\n online: true\n },\n {\n username: 'Laura',\n online: true\n }\n ]\n }\n }\n render() {\n const usersOnline = this.state.users.filter(user => {\n return user.online;\n });\n const renderOnlineUsers = usersOnline.map(user => {\n return (\n{user.username} \n );\n });\n return (\n\n\n );\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "react": true + }, + { + "id": "5a24c314108439a4d403618d", + "title": "Render React on the Server with renderToString", + "releasedOn": "December 25, 2017", + "description": [ + "So far, you have been rendering React components on the client. Normally, this is what you will always do. However, there are some use cases where it makes sense to render a React component on the server. Since React is a JavaScript view library and you can run JavaScript on the server with Node, this is possible. In fact, React provides aCurrent Online Users:
\n\n {renderOnlineUsers}\n
\nrenderToString()method you can use for this purpose.", + "There are two key reasons why rendering on the server may be used in a real world app. First, without doing this, your React apps would consist of a relatively empty HTML file and a large bundle of JavaScript when it's initially loaded to the browser. This may not be ideal for search engines that are trying to index the content of your pages so people can find you. If you render the initial HTML markup on the server and send this to the client, the initial page load contains all of the page's markup which can be crawled by search engines. Second, this creates a faster initial page load experience because the rendered HTML is smaller than the JavaScript code of the entire app. React will still be able to recognize your app and manage it after the initial load.", + "
", + "TherenderToString()method is provided onReactDOMServer, which is available here as a global object. The method takes one argument which is a React element. Use this to renderAppto a string." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "head": [ + "var ReactDOMServer = { renderToString(x) { return null; } };" + ], + "contents": [ + "", + "class App extends React.Component {", + " constructor(props) {", + " super(props);", + " }", + " render() {", + " return ", + " }", + "};", + "", + "// change code below this line", + "" + ], + "tail": "ReactDOM.render(, document.getElementById('root'))" + } + }, + "tests": [ + "getUserInput => assert(getUserInput('index').replace(/ /g,'').includes('ReactDOMServer.renderToString( )') && Enzyme.mount(React.createElement(App)).children().name() === 'div', 'message: The Appcomponent should render to a string usingReactDOMServer.renderToString.');" + ], + "solutions": [ + "\nclass App extends React.Component {\n constructor(props) {\n super(props);\n }\n render() {\n return \n }\n};\n\n// change code below this line\nReactDOMServer.renderToString();" ], "type": "modern", "isRequired": false, diff --git a/seed/challenges/03-front-end-libraries/redux.json b/seed/challenges/03-front-end-libraries/redux.json index cfe180f845e..61d69b4bdb4 100644 --- a/seed/challenges/03-front-end-libraries/redux.json +++ b/seed/challenges/03-front-end-libraries/redux.json @@ -3,31 +3,928 @@ "order": 6, "time": "5 hours", "helpRoom": "Help", + "required": [ + { + "src": "https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js" + }, + { + "src": "https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.2.0/redux-thunk.js" + } + ], "challenges": [ { - "id": "587d7dbd367417b2b2512bb2", - "title": "Introduction to the Redux Challenges", + "id": "5a24c314108439a4d403614b", + "title": "Create a Redux Store", + "releasedOn": "December 25, 2017", "description": [ - [ - "", - "", - "Redux is self-described as a \"predictable state container for JavaScript apps.\" State (as introduced in the React challenges) is a way to manage what is displayed to a user based on that user's inputs or interactions with the application. While it's not necessary to use Redux with React, or vice-versa, the two are often used in conjunction as the features of Redux support applications built with React. Some of these features include:
- An immutable single state store or \"single state of truth\" through which the application makes UI updates
- Logical structure of `actions` carrying data to `reducer` functions that return a new application state
- A trackable history of changes to state
Redux is often added to a project when the complexity of managing the application's state becomes tedious. It helps minimize some of the headaches that can be caused by data mutability or passing state back-and-forth between many React components. Redux also supports debugging with tools like Redux DevTools. Let's jump in to learning how Redux can simplify your React apps.", - "" - ], - [ - "", - "", - "The Redux challenges have not been ported into freeCodeCamp yet. You can visit this link to work through the alpha version of these challenges. If you have feedback, you can open an issue (or pull request) directly on this repository.", - "" - ] + "Redux is a state management framework that can be used with a number of different web technologies, including React.", + "In Redux, there is a single state object that's responsible for the entire state of your application. This means if you had a React app with ten components, and each component had its own local state, the entire state of your app would be defined by a single state object housed in the Reduxstore. This is the first important principle to understand when learning Redux: the Redux store is the single source of truth when it comes to application state.", + "This also means that any time any piece of your app wants to update state, it must do so through the Redux store. The unidirectional data flow makes it easier to track state management in your app.", + "
", + "The Reduxstoreis an object which holds and manages applicationstate. There is a method calledcreateStore()on the Redux object, which you use to create the Reduxstore. This method takes areducerfunction as a required argument. Thereducerfunction is covered in a later challenge, and is already defined for you in the code editor. It simply takesstateas an argument and returnsstate.", + "Declare astorevariable and assign it to thecreateStore()method, passing in thereduceras an argument.", + "Note: The code in the editor uses ES6 default argument syntax to initialize this state to hold a value of5. If you're not familiar with default arguments, you can refer to the ES6 section in the Beta Curriculum which covers this topic." ], - "releasedOn": "Feb 17, 2017", - "challengeSeed": [], - "tests": [], - "type": "waypoint", - "challengeType": 7, + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const reducer = (state = 5) => {", + " return state;", + "}", + "", + "// Redux methods are available from a Redux object", + "// For example: Redux.createStore()", + "// Define the store here:", + "", + "" + ] + } + }, + "tests": [ + "assert(typeof store.getState === 'function', 'message: The redux store exists.');", + "assert(store.getState()=== 5, 'message: The redux store has a value of 5 for the state.');" + ], + "solutions": [ + "const reducer = (state = 5) => {\n return state;\n}\n\n// Redux methods are available from a Redux object\n// For example: Redux.createStore()\n// Define the store here:\n\nconst store = Redux.createStore(reducer);" + ], + "type": "modern", "isRequired": false, - "translations": {} + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d403614c", + "title": "Get State from the Redux Store", + "releasedOn": "December 25, 2017", + "description": [ + "The Redux store object provides several methods that allow you to interact with it. For example, you can retrieve the currentstateheld in the Redux store object with thegetState()method.", + "
", + "The code from the previous challenge is re-written more concisely in the code editor. Usestore.getState()to retrieve thestatefrom thestore, and assign this to a new variablecurrentState." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const store = Redux.createStore(", + " (state = 5) => state", + ");", + "", + "// change code below this line", + "" + ] + } + }, + "tests": [ + "assert(store.getState()===5, 'message: The redux store should have a value of 5 for the initial state.');", + "getUserInput => assert(currentState === 5 && getUserInput('index').includes('store.getState()'), 'message: A variablecurrentStateshould exist and should be assigned the current state of the Redux store.');" + ], + "solutions": [ + "const store = Redux.createStore(\n (state = 5) => state\n);\n\n// change code below this line\nconst currentState = store.getState();" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d403614d", + "title": "Define a Redux Action", + "releasedOn": "December 25, 2017", + "description": [ + "Since Redux is a state management framework, updating state is one of its core tasks. In Redux, all state updates are triggered by dispatching actions. An action is simply a JavaScript object that contains information about an action event that has occurred. The Redux store receives these action objects, then updates its state accordingly. Sometimes a Redux action also carries some data. For example, the action carries a username after a user logs in. While the data is optional, actions must carry atypeproperty that specifies the 'type' of action that occurred.", + "Think of Redux actions as messengers that deliver information about events happening in your app to the Redux store. The store then conducts the business of updating state based on the action that occurred.", + "
", + "Writing a Redux action is as simple as declaring an object with a type property. Declare an objectactionand give it a propertytypeset to the string'LOGIN'." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "// Define an action here:", + "" + ] + } + }, + "tests": [ + "assert((function() { return typeof action === 'object' })(), 'message: An action object should exist.');", + "assert((function() { return action.type === 'LOGIN' })(), 'message: The action should have a key property type with valueLOGIN.');" + ], + "solutions": [ + "const action = {\n type: 'LOGIN'\n}" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d403614e", + "title": "Define an Action Creator", + "releasedOn": "December 25, 2017", + "description": [ + "After creating an action, the next step is sending the action to the Redux store so it can update its state. In Redux, you define action creators to accomplish this. An action creator is simply a JavaScript function that returns an action. In other words, action creators create objects that represent action events.", + "
", + "Define a function namedactionCreator()that returns theactionobject when called." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const action = {", + " type: 'LOGIN'", + "}", + "// Define an action creator here:", + "" + ] + } + }, + "tests": [ + "assert(typeof actionCreator === 'function', 'message: The functionactionCreatorshould exist.');", + "assert(typeof action === 'object', 'message: Running theactionCreatorfunction should return the action object.');", + "assert(action.type === 'LOGIN', 'message: The returned action should have a key property type with valueLOGIN.');" + ], + "solutions": [ + "const action = {\n type: 'LOGIN'\n}\n// Define an action creator here:\nconst actionCreator = () => {\n return action;\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d403614f", + "title": "Dispatch an Action Event", + "releasedOn": "December 25, 2017", + "description": [ + "od is what you use to dispatch actions to the Redux store. Callingstore.dispatch()and passing the value returned from an action creator sends an action back to the store.", + "Recall that action creators return an object with a type property that specifies the action that has occurred. Then the method dispatches an action object to the Redux store. Based on the previous challenge's example, the following lines are equivalent, and both dispatch the action of typeLOGIN:", + "store.dispatch(actionCreator());", + "
store.dispatch({ type: 'LOGIN' });
", + "The Redux store in the code editor has an initialized state that's an object containing aloginproperty currently set tofalse. There's also an action creator calledloginAction()which returns an action of typeLOGIN. Dispatch theLOGINaction to the Redux store by calling thedispatchmethod, and pass in the action created byloginAction()." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const store = Redux.createStore(", + " (state = {login: false}) => state", + ");", + "", + "const loginAction = () => {", + " return {", + " type: 'LOGIN'", + " }", + "};", + "", + "// Dispatch the action here:", + "" + ] + } + }, + "tests": [ + "assert(loginAction().type === 'LOGIN', 'message: Calling the functionloginActionshould return an object withtypeproperty set to the stringLOGIN.');", + "assert(store.getState().login === false, 'message: The store should be initialized with an object with propertyloginset tofalse.');", + "getUserInput => assert((function() { let noWhiteSpace = getUserInput('index').replace(/ /g,''); return noWhiteSpace.includes('store.dispatch(loginAction())') || noWhiteSpace.includes('store.dispatch({type: \\'LOGIN\\'})') === true })(), 'message: Thestore.dispatch()method should be used to dispatch an action of typeLOGIN.');" + ], + "solutions": [ + "const store = Redux.createStore(\n (state = {login: false}) => state\n);\n\nconst loginAction = () => {\n return {\n type: 'LOGIN'\n }\n};\n\n// Dispatch the action here:\nstore.dispatch(loginAction());" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036150", + "title": "Handle an Action in the Store", + "releasedOn": "December 25, 2017", + "description": [ + "After an action is created and dispatched, the Redux store needs to know how to respond to that action. This is the job of areducerfunction. Reducers in Redux are responsible for the state modifications that take place in response to actions. Areducertakesstateandactionas arguments, and it always returns a newstate. It is important to see that this is the only role of the reducer. It has no side effects — it never calls an API endpoint and it never has any hidden surprises. The reducer is simply a pure function that takes state and action, then returns new state.", + "Another key principle in Redux is thatstateis read-only. In other words, thereducerfunction must always return a new copy ofstateand never modify state directly. Redux does not enforce state immutability, however, you are responsible for enforcing it in the code of your reducer functions. You'll practice this in later challenges.", + "
", + "The code editor has the previous example as well as the start of areducerfunction for you. Fill in the body of thereducerfunction so that if it receives an action of type'LOGIN'it returns a state object withloginset totrue. Otherwise, it returns the currentstate. Note that the currentstateand the dispatchedactionare passed to the reducer, so you can access the action's type directly withaction.type." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const defaultState = {", + " login: false", + "};", + "", + "const reducer = (state = defaultState, action) => {", + " // change code below this line", + "", + " // change code above this line", + "};", + "", + "const store = Redux.createStore(reducer);", + "", + "const loginAction = () => {", + " return {", + " type: 'LOGIN'", + " }", + "};" + ] + } + }, + "tests": [ + "assert(loginAction().type === 'LOGIN', 'message: Calling the functionloginActionshould return an object with type property set to the stringLOGIN.');", + "assert(store.getState().login === false, 'message: The store should be initialized with an object with propertyloginset tofalse.');", + "assert((function() { const initialState = store.getState(); store.dispatch(loginAction()); const afterState = store.getState(); return initialState.login === false && afterState.login === true })(), 'message: DispatchingloginActionshould update theloginproperty in the store state totrue.');", + "assert((function() { store.dispatch({type: '__TEST__ACTION__'}); let afterTest = store.getState(); return typeof afterTest === 'object' && afterTest.hasOwnProperty('login') })(), 'message: If the action is not of typeLOGIN, the store should return the current state.');" + ], + "solutions": [ + "const defaultState = {\n login: false\n};\n\nconst reducer = (state = defaultState, action) => {\n\n if (action.type === 'LOGIN') {\n return {login: true}\n }\n\n else {\n return state\n }\n\n};\n\nconst store = Redux.createStore(reducer);\n\nconst loginAction = () => {\n return {\n type: 'LOGIN'\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036151", + "title": "Use a Switch Statement to Handle Multiple Actions", + "releasedOn": "December 25, 2017", + "description": [ + "You can tell the Redux store how to handle multiple action types. Say you are managing user authentication in your Redux store. You want to have a state representation for when users are logged in and when they are logged out. You represent this with a single state object with the propertyauthenticated. You also need action creators that create actions corresponding to user login and user logout, along with the action objects themselves.", + "
", + "The code editor has a store, actions, and action creators set up for you. Fill in thereducerfunction to handle multiple authentication actions. Use a JavaScriptswitchstatement in thereducerto respond to different action events. This is a standard pattern in writing Redux reducers. The switch statement should switch overaction.typeand return the appropriate authentication state.", + "Note: At this point, don't worry about state immutability, since it is small and simple in this example. For each action, you can return a new object — for example,{authenticated: true}. Also, don't forget to write adefaultcase in your switch statement that returns the currentstate. This is important because once your app has multiple reducers, they are all run any time an action dispatch is made, even when the action isn't related to that reducer. In such a case, you want to make sure that you return the currentstate." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const defaultState = {", + " authenticated: false", + "};", + "", + "const authReducer = (state = defaultState, action) => {", + " // change code below this line", + "", + " // change code above this line", + "};", + "", + "const store = Redux.createStore(authReducer);", + "", + "const loginUser = () => {", + " return {", + " type: 'LOGIN'", + " }", + "};", + "", + "const logoutUser = () => {", + " return {", + " type: 'LOGOUT'", + " }", + "};" + ] + } + }, + "tests": [ + "assert(loginUser().type === 'LOGIN', 'message: Calling the functionloginUsershould return an object with type property set to the stringLOGIN.');", + "assert(logoutUser().type === 'LOGOUT', 'message: Calling the functionlogoutUsershould return an object with type property set to the stringLOGOUT.');", + "assert(store.getState().authenticated === false, 'message: The store should be initialized with an object with anauthenticatedproperty set tofalse.');", + "assert((function() { const initialState = store.getState(); store.dispatch(loginUser()); const afterLogin = store.getState(); return initialState.authenticated === false && afterLogin.authenticated === true })(), 'message: DispatchingloginUsershould update theauthenticatedproperty in the store state totrue.');", + "assert((function() { store.dispatch(loginUser()); const loggedIn = store.getState(); store.dispatch(logoutUser()); const afterLogout = store.getState(); return loggedIn.authenticated === true && afterLogout.authenticated === false })(), 'message: DispatchinglogoutUsershould update theauthenticatedproperty in the store state tofalse.');", + "getUserInput => assert( getUserInput('index').toString().includes('switch') && getUserInput('index').toString().includes('case') && getUserInput('index').toString().includes('default'), 'message: TheauthReducerfunction should handle multiple action types with aswitchstatement.');" + ], + "solutions": [ + "const defaultState = {\n authenticated: false\n};\n\nconst authReducer = (state = defaultState, action) => {\n\n switch (action.type) {\n\n case 'LOGIN':\n return {\n authenticated: true\n }\n\n case 'LOGOUT':\n return {\n authenticated: false\n }\n\n default:\n return state;\n\n }\n\n};\n\nconst store = Redux.createStore(authReducer);\n\nconst loginUser = () => {\n return {\n type: 'LOGIN'\n }\n};\n\nconst logoutUser = () => {\n return {\n type: 'LOGOUT'\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036152", + "title": "Use const for Action Types", + "releasedOn": "December 25, 2017", + "description": [ + "A common practice when working with Redux is to assign action types as read-only constants, then reference these constants wherever they are used. You can refactor the code you're working with to write the action types asconstdeclarations.", + "
", + "DeclareLOGINandLOGOUTasconstvalues and assign them to the strings'LOGIN'and'LOGOUT', respectively. Then, edit theauthReducer()and the action creators to reference these constants instead of string values.", + "Note: It's generally a convention to write constants in all uppercase, and this is standard practice in Redux as well." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "// change code below this line", + "", + "// change code above this line", + "", + "const defaultState = {", + " authenticated: false", + "};", + "", + "const authReducer = (state = defaultState, action) => {", + "", + " switch (action.type) {", + "", + " case 'LOGIN':", + " return {", + " authenticated: true", + " }", + "", + " case 'LOGOUT':", + " return {", + " authenticated: false", + " }", + "", + " default:", + " return state;", + "", + " }", + "", + "};", + "", + "const store = Redux.createStore(authReducer);", + "", + "const loginUser = () => {", + " return {", + " type: 'LOGIN'", + " }", + "};", + "", + "const logoutUser = () => {", + " return {", + " type: 'LOGOUT'", + " }", + "};" + ] + } + }, + "tests": [ + "assert(loginUser().type === 'LOGIN', 'message: Calling the functionloginUsershould return an object withtypeproperty set to the stringLOGIN.');", + "assert(logoutUser().type === 'LOGOUT', 'message: Calling the functionlogoutUsershould return an object withtypeproperty set to the stringLOGOUT.');", + "assert(store.getState().authenticated === false, 'message: The store should be initialized with an object with propertyloginset tofalse.');", + "assert((function() { const initialState = store.getState(); store.dispatch(loginUser()); const afterLogin = store.getState(); return initialState.authenticated === false && afterLogin.authenticated === true })(), 'message: DispatchingloginUsershould update theloginproperty in the store state totrue.');", + "assert((function() { store.dispatch(loginUser()); const loggedIn = store.getState(); store.dispatch(logoutUser()); const afterLogout = store.getState(); return loggedIn.authenticated === true && afterLogout.authenticated === false })(), 'message: DispatchinglogoutUsershould update theloginproperty in the store state tofalse.');", + "getUserInput => assert((function() { return typeof authReducer === 'function' && getUserInput('index').toString().includes('switch') && getUserInput('index').toString().includes('case') && getUserInput('index').toString().includes('default') })(), 'message: TheauthReducerfunction should handle multiple action types with a switch statement.');", + "getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').toString().replace(/ /g,''); return (noWhiteSpace.includes('constLOGIN=\\'LOGIN\\'') || noWhiteSpace.includes('constLOGIN=\"LOGIN\"')) && (noWhiteSpace.includes('constLOGOUT=\\'LOGOUT\\'') || noWhiteSpace.includes('constLOGOUT=\"LOGOUT\"')) })(), 'message:LOGINandLOGOUTshould be declared asconstvalues and should be assigned strings ofLOGINandLOGOUT.');", + "getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').toString().replace(/ /g,''); return noWhiteSpace.includes('caseLOGIN:') && noWhiteSpace.includes('caseLOGOUT:') && noWhiteSpace.includes('type:LOGIN') && noWhiteSpace.includes('type:LOGOUT') })(), 'message: The action creators and the reducer should reference theLOGINandLOGOUTconstants.');" + ], + "solutions": [ + "const LOGIN = 'LOGIN';\nconst LOGOUT = 'LOGOUT';\n\nconst defaultState = {\n authenticated: false\n};\n\nconst authReducer = (state = defaultState, action) => {\n\n switch (action.type) {\n\n case LOGIN:\n return {\n authenticated: true\n }\n\n case LOGOUT:\n return {\n authenticated: false\n }\n\n default:\n return state;\n\n }\n\n};\n\nconst store = Redux.createStore(authReducer);\n\nconst loginUser = () => {\n return {\n type: LOGIN\n }\n};\n\nconst logoutUser = () => {\n return {\n type: LOGOUT\n }\n};" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036153", + "title": "Register a Store Listener", + "releasedOn": "December 25, 2017", + "description": [ + "Another method you have access to on the Reduxstoreobject isstore.subscribe(). This allows you to subscribe listener functions to the store, which are called whenever an action is dispatched against the store. One simple use for this method is to subscribe a function to your store that simply logs a message every time an action is received and the store is updated.", + "
", + "Write a callback function that increments the global variablecountevery time the store receives an action, and pass this function in to thestore.subscribe()method. You'll see thatstore.dispatch()is called three times in a row, each time directly passing in an action object. Watch the console output between the action dispatches to see the updates take place." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "head": [ + "count = 0;" + ], + "contents": [ + "const ADD = 'ADD';", + "", + "const reducer = (state = 0, action) => {", + " switch(action.type) {", + " case ADD:", + " return state + 1;", + " default:", + " return state;", + " }", + "};", + "", + "const store = Redux.createStore(reducer);", + "", + "// global count variable:", + "let count = 0;", + "", + "// change code below this line", + "", + "// change code above this line", + "", + "store.dispatch({type: ADD});", + "console.log(count);", + "store.dispatch({type: ADD});", + "console.log(count);", + "store.dispatch({type: ADD});", + "console.log(count);" + ] + } + }, + "tests": [ + "assert((function() { const initialState = store.getState(); store.dispatch({ type: 'ADD' }); const newState = store.getState(); return newState === (initialState + 1); })(), 'message: Dispatching theADDaction on the store should increment the state by1.');", + "getUserInput => assert(getUserInput('index').includes('store.subscribe('), 'message: There should be a listener function subscribed to the store usingstore.subscribe.');", + "assert(store.getState() === count, 'message: The callback tostore.subscribeshould also increment the globalcountvariable as the store is updated.');" + ], + "solutions": [ + "const ADD = 'ADD';\n\nconst reducer = (state = 0, action) => {\n switch(action.type) {\n case ADD:\n return state + 1;\n default:\n return state;\n }\n};\n\nconst store = Redux.createStore(reducer);\n let count = 0; \n// change code below this line\n\nstore.subscribe( () =>\n { \n count++; \n } \n);\n\n// change code above this line\n\nstore.dispatch({type: ADD});\nstore.dispatch({type: ADD});\nstore.dispatch({type: ADD});" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036154", + "title": "Combine Multiple Reducers", + "releasedOn": "December 25, 2017", + "description": [ + "When the state of your app begins to grow more complex, it may be tempting to divide state into multiple pieces. Instead, remember the first principle of Redux: all app state is held in a single state object in the store. Therefore, Redux provides reducer composition as a solution for a complex state model. You define multiple reducers to handle different pieces of your application's state, then compose these reducers together into one root reducer. The root reducer is then passed into the ReduxcreateStore()method.", + "In order to let us combine multiple reducers together, Redux provides thecombineReducers()method. This method accepts an object as an argument in which you define properties which associate keys to specific reducer functions. The name you give to the keys will be used by Redux as the name for the associated piece of state.", + "Typically, it is a good practice to create a reducer for each piece of application state when they are distinct or unique in some way. For example, in a note-taking app with user authentication, one reducer could handle authentication while another handles the text and notes that the user is submitting. For such an application, we might write thecombineReducers()method like this:", + "const rootReducer = Redux.combineReducers({", + "Now, the key
auth: authenticationReducer,
notes: notesReducer
});noteswill contain all of the state associated with our notes and handled by ournotesReducer. This is how multiple reducers can be composed to manage more complex application state. In this example, the state held in the Redux store would then be a single object containingauthandnotesproperties.", + "
", + "There arecounterReducer()andauthReducer()functions provided in the code editor, along with a Redux store. Finish writing therootReducer()function using theRedux.combineReducers()method. AssigncounterReducerto a key calledcountandauthReducerto a key calledauth." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const INCREMENT = 'INCREMENT';", + "const DECREMENT = 'DECREMENT';", + "", + "const counterReducer = (state = 0, action) => {", + " switch(action.type) {", + " case INCREMENT:", + " return state + 1;", + " case DECREMENT:", + " return state - 1;", + " default:", + " return state;", + " }", + "};", + "", + "const LOGIN = 'LOGIN';", + "const LOGOUT = 'LOGOUT';", + "", + "const authReducer = (state = {authenticated: false}, action) => {", + " switch(action.type) {", + " case LOGIN:", + " return {", + " authenticated: true", + " }", + " case LOGOUT:", + " return {", + " authenticated: false", + " }", + " default:", + " return state;", + " }", + "};", + "", + "const rootReducer = // define the root reducer here", + "", + "const store = Redux.createStore(rootReducer);", + "" + ] + } + }, + "tests": [ + "assert((function() { const initalState = store.getState().count; store.dispatch({type: INCREMENT}); store.dispatch({type: INCREMENT}); const firstState = store.getState().count; store.dispatch({type: DECREMENT}); const secondState = store.getState().count; return firstState === initalState + 2 && secondState === firstState - 1 })(), 'message: ThecounterReducershould increment and decrement thestate.');", + "assert((function() { store.dispatch({type: LOGIN}); const loggedIn = store.getState().auth.authenticated; store.dispatch({type: LOGOUT}); const loggedOut = store.getState().auth.authenticated; return loggedIn === true && loggedOut === false })(), 'message: TheauthReducershould toggle thestateofauthenticatedbetweentrueandfalse.');", + "assert((function() { const state = store.getState(); return typeof state.auth === 'object' && typeof state.auth.authenticated === 'boolean' && typeof state.count === 'number' })(), 'message: The storestateshould have two keys:count, which holds a number, andauth, which holds an object. Theauthobject should have a property ofauthenticated, which holds a boolean.');", + "getUserInput => assert((function() { const noWhiteSpace = getUserInput('index').replace(/ /g,''); return typeof rootReducer === 'function' && noWhiteSpace.includes('Redux.combineReducers') })(), 'message: TherootReducershould be a function that combines thecounterReducerand theauthReducer.');" + ], + "solutions": [ + "const INCREMENT = 'INCREMENT';\nconst DECREMENT = 'DECREMENT';\n\nconst counterReducer = (state = 0, action) => {\n switch(action.type) {\n case INCREMENT:\n return state + 1;\n case DECREMENT:\n return state - 1;\n default:\n return state;\n }\n};\n\nconst LOGIN = 'LOGIN';\nconst LOGOUT = 'LOGOUT';\n\nconst authReducer = (state = {authenticated: false}, action) => {\n switch(action.type) {\n case LOGIN:\n return {\n authenticated: true\n }\n case LOGOUT:\n return {\n authenticated: false\n }\n default:\n return state;\n }\n};\n\nconst rootReducer = Redux.combineReducers({\n count: counterReducer,\n auth: authReducer\n});\n\nconst store = Redux.createStore(rootReducer);" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036155", + "title": "Send Action Data to the Store", + "releasedOn": "December 25, 2017", + "description": [ + "By now you've learned how to dispatch actions to the Redux store, but so far these actions have not contained any information other than atype. You can also send specific data along with your actions. In fact, this is very common because actions usually originate from some user interaction and tend to carry some data with them. The Redux store often needs to know about this data.", + "
", + "There's a basicnotesReducer()and anaddNoteText()action creator defined in the code editor. Finish the body of theaddNoteText()function so that it returns anactionobject. The object should include atypeproperty with a value ofADD_NOTE, and also atextproperty set to thenotedata that's passed into the action creator. When you call the action creator, you'll pass in specific note information that you can access for the object.", + "Next, finish writing theswitchstatement in thenotesReducer(). You need to add a case that handles theaddNoteText()actions. This case should be triggered whenever there is an action of typeADD_NOTEand it should return thetextproperty on the incomingactionas the newstate.", + "The action is dispatched at the bottom of the code. Once you're finished, run the code and watch the console. That's all it takes to send action-specific data to the store and use it when you update storestate." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const ADD_NOTE = 'ADD_NOTE';", + "", + "const notesReducer = (state = 'Initial State', action) => {", + " switch(action.type) {", + " // change code below this line", + "", + " // change code above this line", + " default:", + " return state;", + " }", + "};", + "", + "const addNoteText = (note) => {", + " // change code below this line", + "", + " // change code above this line", + "};", + "", + "const store = Redux.createStore(notesReducer);", + "", + "console.log(store.getState());", + "store.dispatch(addNoteText('Hello!'));", + "console.log(store.getState());" + ] + } + }, + "tests": [ + "assert((function() { const addNoteFn = addNoteText('__TEST__NOTE'); return addNoteFn.type === ADD_NOTE && addNoteFn.text === '__TEST__NOTE' })(), 'message: The action creatoraddNoteTextshould return an object with keystypeandtext.');", + "assert((function() { const initialState = store.getState(); store.dispatch(addNoteText('__TEST__NOTE')); const newState = store.getState(); return initialState !== newState && newState === '__TEST__NOTE' })(), 'message: Dispatching an action of typeADD_NOTEwith theaddNoteTextaction creator should update thestateto the string passed to the action creator.');" + ], + "solutions": [ + "const ADD_NOTE = 'ADD_NOTE';\n\nconst notesReducer = (state = 'Initial State', action) => {\n switch(action.type) {\n // change code below this line\n case ADD_NOTE:\n return action.text;\n // change code above this line\n default:\n return state;\n }\n};\n\nconst addNoteText = (note) => {\n // change code below this line\n return {\n type: ADD_NOTE,\n text: note\n }\n // change code above this line\n};\n\nconst store = Redux.createStore(notesReducer);\n\nconsole.log(store.getState());\nstore.dispatch(addNoteText('Hello Redux!'));\nconsole.log(store.getState());" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036156", + "title": "Use Middleware to Handle Asynchronous Actions", + "releasedOn": "December 25, 2017", + "description": [ + "So far these challenges have avoided discussing asynchronous actions, but they are an unavoidable part of web development. At some point you'll need to call asynchronous endpoints in your Redux app, so how do you handle these types of requests? Redux provides middleware designed specifically for this purpose, called Redux Thunk middleware. Here's a brief description how to use this with Redux.", + "To include Redux Thunk middleware, you pass it as an argument toRedux.applyMiddleware(). This statement is then provided as a second optional parameter to thecreateStore()function. Take a look at the code at the bottom of the editor to see this. Then, to create an asynchronous action, you return a function in the action creator that takesdispatchas an argument. Within this function, you can dispatch actions and perform asynchronous requests.", + "In this example, an asynchronous request is simulated with asetTimeout()call. It's common to dispatch an action before initiating any asynchronous behavior so that your application state knows that some data is being requested (this state could display a loading icon, for instance). Then, once you receive the data, you dispatch another action which carries the data as a payload along with information that the action is completed.", + "Remember that you're passingdispatchas a parameter to this special action creator. This is what you'll use to dispatch your actions, you simply pass the action directly to dispatch and the middleware takes care of the rest.", + "
", + "Write both dispatches in thehandleAsync()action creator. DispatchrequestingData()before thesetTimeout()(the simulated API call). Then, after you receive the (pretend) data, dispatch thereceivedData()action, passing in this data. Now you know how to handle asynchronous actions in Redux. Everything else continues to behave as before." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const REQUESTING_DATA = 'REQUESTING_DATA'", + "const RECEIVED_DATA = 'RECEIVED_DATA'", + "", + "const requestingData = () => { return {type: REQUESTING_DATA} }", + "const receivedData = (data) => { return {type: RECEIVED_DATA, users: data.users} }", + "", + "const handleAsync = () => {", + " return function(dispatch) {", + " // dispatch request action here", + "", + " setTimeout(function() {", + " let data = {", + " users: ['Jeff', 'William', 'Alice']", + " }", + " // dispatch received data action here", + "", + " }, 2500);", + " }", + "};", + "", + "const defaultState = {", + " fetching: false,", + " users: []", + "};", + "", + "const asyncDataReducer = (state = defaultState, action) => {", + " switch(action.type) {", + " case REQUESTING_DATA:", + " return {", + " fetching: true,", + " users: []", + " }", + " case RECEIVED_DATA:", + " return {", + " fetching: false,", + " users: action.users", + " }", + " default:", + " return state;", + " }", + "};", + "", + "const store = Redux.createStore(", + " asyncDataReducer,", + " Redux.applyMiddleware(ReduxThunk.default)", + ");" + ] + } + }, + "tests": [ + "assert(requestingData().type === REQUESTING_DATA, 'message: TherequestingDataaction creator should return an object of type equal to the value ofREQUESTING_DATA.');", + "assert(receivedData('data').type === RECEIVED_DATA, 'message: ThereceivedDataaction creator should return an object of type equal to the value ofRECEIVED_DATA.');", + "assert(typeof asyncDataReducer === 'function', 'message:asyncDataReducershould be a function.');", + "assert((function() { const initialState = store.getState(); store.dispatch(requestingData()); const reqState = store.getState(); return initialState.fetching === false && reqState.fetching === true })(), 'message: Dispatching the requestingData action creator should update the storestateproperty of fetching totrue.');", + "assert((function() { const noWhiteSpace = handleAsync.toString().replace(/ /g,''); return noWhiteSpace.includes('dispatch(requestingData())') === true && noWhiteSpace.includes('dispatch(receivedData(data))') === true })(), 'message: DispatchinghandleAsyncshould dispatch the data request action and then dispatch the received data action after a delay.');" + ], + "solutions": [ + "const REQUESTING_DATA = 'REQUESTING_DATA'\nconst RECEIVED_DATA = 'RECEIVED_DATA'\n\nconst requestingData = () => { return {type: REQUESTING_DATA} }\nconst receivedData = (data) => { return {type: RECEIVED_DATA, users: data.users} }\n\nconst handleAsync = () => {\n return function(dispatch) {\n dispatch(requestingData());\n setTimeout(function() {\n let data = {\n users: ['Jeff', 'William', 'Alice']\n }\n dispatch(receivedData(data));\n }, 2500);\n }\n};\n\nconst defaultState = {\n fetching: false,\n users: []\n};\n\nconst asyncDataReducer = (state = defaultState, action) => {\n switch(action.type) {\n case REQUESTING_DATA:\n return {\n fetching: true,\n users: []\n }\n case RECEIVED_DATA:\n return {\n fetching: false,\n users: action.users\n }\n default:\n return state;\n }\n};\n\nconst store = Redux.createStore(\n asyncDataReducer,\n Redux.applyMiddleware(ReduxThunk.default)\n);" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036157", + "title": "Write a Counter with Redux", + "releasedOn": "December 25, 2017", + "description": [ + "Now you've learned all the core principles of Redux! You've seen how to create actions and action creators, create a Redux store, dispatch your actions against the store, and design state updates with pure reducers. You've even seen how to manage complex state with reducer composition and handle asynchronous actions. These examples are simplistic, but these concepts are the core principles of Redux. If you understand them well, you're ready to start building your own Redux app. The next challenges cover some of the details regardingstateimmutability, but first, here's a review of everything you've learned so far.", + "
", + "In this lesson, you'll implement a simple counter with Redux from scratch. The basics are provided in the code editor, but you'll have to fill in the details! Use the names that are provided and defineincActionanddecActionaction creators, thecounterReducer(),INCREMENTandDECREMENTaction types, and finally the Reduxstore. Once you're finished you should be able to dispatchINCREMENTorDECREMENTactions to increment or decrement the state held in thestore. Good luck building your first Redux app!" + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const INCREMENT = // define a constant for increment action types", + "const DECREMENT = // define a constant for decrement action types", + "", + "const counterReducer = // define the counter reducer which will increment or decrement the state based on the action it receives", + "", + "const incAction = // define an action creator for incrementing", + "", + "const decAction = // define an action creator for decrementing", + "", + "const store = // define the Redux store here, passing in your reducers" + ] + } + }, + "tests": [ + "assert(incAction().type ===INCREMENT, 'message: The action creatorincActionshould return an action object withtypeequal to the value ofINCREMENT');", + "assert(decAction().type === DECREMENT, 'message: The action creatordecActionshould return an action object withtypeequal to the value ofDECREMENT');", + "assert(store.getState() === 0, 'message: The Redux store should initialize with astateof 0.');", + "assert((function() { const initialState = store.getState(); store.dispatch(incAction()); const incState = store.getState(); return initialState + 1 === incState })(), 'message: DispatchingincActionon the Redux store should increment thestateby 1.');", + "assert((function() { const initialState = store.getState(); store.dispatch(decAction()); const decState = store.getState(); return initialState - 1 === decState })(), 'message: DispatchingdecActionon the Redux store should decrement thestateby 1.');", + "assert(typeof counterReducer === 'function', 'message:counterReducershould be a function');" + ], + "solutions": [ + "const INCREMENT = 'INCREMENT';\nconst DECREMENT = 'DECREMENT';\n\nconst counterReducer = (state = 0, action) => {\n switch(action.type) {\n case INCREMENT:\n return state + 1;\n case DECREMENT:\n return state - 1;\n default:\n return state;\n }\n};\n\nconst incAction = () => {\n return {\n type: INCREMENT\n }\n};\n\nconst decAction = () => {\n return {\n type: DECREMENT\n }\n};\n\nconst store = Redux.createStore(counterReducer);" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036158", + "title": "Never Mutate State", + "releasedOn": "December 25, 2017", + "description": [ + "These final challenges describe several methods of enforcing the key principle of state immutability in Redux. Immutable state means that you never modify state directly, instead, you return a new copy of state.", + "If you took a snapshot of the state of a Redux app over time, you would see something likestate 1,state 2,state 3,state 4,...and so on where each state may be similar to the last, but each is a distinct piece of data. This immutability, in fact, is what provides such features as time-travel debugging that you may have heard about.", + "Redux does not actively enforce state immutability in its store or reducers, that responsibility falls on the programmer. Fortunately, JavaScript (especially ES6) provides several useful tools you can use to enforce the immutability of your state, whether it is astring,number,array, orobject. Note that strings and numbers are primitive values and are immutable by nature. In other words, 3 is always 3. You cannot change the value of the number 3. Anarrayorobject, however, is mutable. In practice, your state will probably consist of anarrayorobject, as these are useful data structures for representing many types of information.", + "
", + "There is astoreandreducerin the code editor for managing to-do items. Finish writing theADD_TO_DOcase in the reducer to append a new to-do to the state. There are a few ways to accomplish this with standard JavaScript or ES6. See if you can find a way to return a new array with the item fromaction.todoappended to the end." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const ADD_TO_DO = 'ADD_TO_DO';", + "", + "// A list of strings representing tasks to do:", + "const todos = [", + " 'Go to the store',", + " 'Clean the house',", + " 'Cook dinner',", + " 'Learn to code',", + "];", + "", + "const immutableReducer = (state = todos, action) => {", + " switch(action.type) {", + " case ADD_TO_DO:", + " // don't mutate state here or the tests will fail", + " return", + " default:", + " return state;", + " }", + "};", + "", + "// an example todo argument would be 'Learn React',", + "const addToDo = (todo) => {", + " return {", + " type: ADD_TO_DO,", + " todo", + " }", + "}", + "", + "const store = Redux.createStore(immutableReducer);" + ] + } + }, + "tests": [ + "assert((function() { const todos = [ 'Go to the store', 'Clean the house', 'Cook dinner', 'Learn to code' ]; const initialState = store.getState(); return Array.isArray(initialState) && initialState.join(',') === todos.join(','); })(), 'message: The Redux store should exist and initialize with a state equal to thetodosarray in the code editor.');", + "assert(typeof addToDo === 'function' && typeof immutableReducer === 'function', 'message:addToDoandimmutableReducerboth should be functions.');", + "assert((function() { const initialState = store.getState(); const isFrozen = DeepFreeze(initialState); store.dispatch(addToDo('__TEST__TO__DO__')); const finalState = store.getState(); const expectedState = [ 'Go to the store', 'Clean the house', 'Cook dinner', 'Learn to code', '__TEST__TO__DO__' ]; return( isFrozen && DeepEqual(finalState, expectedState)); })(), 'message: Dispatching an action of typeADD_TO_DOon the Redux store should add atodoitem and should NOT mutate state.');" + ], + "solutions": [ + "const ADD_TO_DO = 'ADD_TO_DO';\n\n// A list of strings representing tasks to do:\nconst todos = [\n 'Go to the store',\n 'Clean the house',\n 'Cook dinner',\n 'Learn to code',\n];\n\nconst immutableReducer = (state = todos, action) => {\n switch(action.type) {\n case ADD_TO_DO:\n return state.concat(action.todo);\n default:\n return state;\n }\n};\n\n// an example todo argument would be 'Learn React',\nconst addToDo = (todo) => {\n return {\n type: ADD_TO_DO,\n todo\n }\n}\n\nconst store = Redux.createStore(immutableReducer);" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d4036159", + "title": "Use the Spread Operator on Arrays", + "releasedOn": "December 25, 2017", + "description": [ + "One solution from ES6 to help enforce state immutability in Redux is the spread operator:.... The spread operator has a variety of applications, one of which is well-suited to the previous challenge of producing a new array from an existing array. This is relatively new, but commonly used syntax. For example, if you have an arraymyArrayand write:", + "let newArray = [...myArray];", + "newArrayis now a clone ofmyArray. Both arrays still exist separately in memory. If you perform a mutation likenewArray.push(5),myArraydoesn't change. The...effectively spreads out the values inmyArrayinto a new array. To clone an array but add additional values in the new array, you could write[...myArray, 'new value']. This would return a new array composed of the values inmyArrayand the string'new value'as the last value. The spread syntax can be used multiple times in array composition like this, but it's important to note that it only makes a shallow copy of the array. That is to say, it only provides immutable array operations for one-dimensional arrays.", + "
", + "Use the spread operator to return a new copy of state when a to-do is added." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const immutableReducer = (state = ['Do not mutate state!'], action) => {", + " switch(action.type) {", + " case 'ADD_TO_DO':", + " // don't mutate state here or the tests will fail", + " return", + " default:", + " return state;", + " }", + "};", + "", + "const addToDo = (todo) => {", + " return {", + " type: 'ADD_TO_DO',", + " todo", + " }", + "}", + "", + "const store = Redux.createStore(immutableReducer);" + ] + } + }, + "tests": [ + "assert((function() { const initialState = store.getState(); return ( Array.isArray(initialState) === true && initialState[0] === 'Do not mutate state!'); })(), 'message: The Redux store should exist and initialize with a state equal to[Do not mutate state!].');", + "assert(typeof addToDo === 'function' && typeof immutableReducer === 'function', 'message:addToDoandimmutableReducerboth should be functions.');", + "assert((function() { const initialState = store.getState(); const isFrozen = DeepFreeze(initialState); store.dispatch(addToDo('__TEST__TO__DO__')); const finalState = store.getState(); const expectedState = [ 'Do not mutate state!', '__TEST__TO__DO__' ]; return( isFrozen && DeepEqual(finalState, expectedState)); })(), 'message: Dispatching an action of typeADD_TO_DOon the Redux store should add atodoitem and should NOT mutate state.');", + "getUserInput => assert(getUserInput('index').includes('...state'), 'message: The spread operator should be used to return new state.');" + ], + "solutions": [ + "const immutableReducer = (state = ['Do not mutate state!'], action) => {\n switch(action.type) {\n case 'ADD_TO_DO':\n return [\n ...state,\n action.todo\n ];\n default:\n return state;\n }\n};\n\nconst addToDo = (todo) => {\n return {\n type: 'ADD_TO_DO',\n todo\n }\n}\n\nconst store = Redux.createStore(immutableReducer);" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d403615a", + "title": "Remove an Item from an Array", + "releasedOn": "December 25, 2017", + "description": [ + "Time to practice removing items from an array. The spread operator can be used here as well. Other useful JavaScript methods includeslice()andconcat().", + "
", + "The reducer and action creator were modified to remove an item from an array based on the index of the item. Finish writing the reducer so a new state array is returned with the item at the specific index removed." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const immutableReducer = (state = [0,1,2,3,4,5], action) => {", + " switch(action.type) {", + " case 'REMOVE_ITEM':", + " // don't mutate state here or the tests will fail", + " return", + " default:", + " return state;", + " }", + "};", + "", + "const removeItem = (index) => {", + " return {", + " type: 'REMOVE_ITEM',", + " index", + " }", + "}", + "", + "const store = Redux.createStore(immutableReducer);" + ] + } + }, + "tests": [ + "assert((function() { const initialState = store.getState(); return (Array.isArray(initialState) === true && DeepEqual(initialState, [0, 1, 2, 3, 4, 5])); })(), 'message: The Redux store should exist and initialize with a state equal to[0,1,2,3,4,5]');", + "assert(typeof removeItem === 'function' && typeof immutableReducer === 'function', 'message:removeItemandimmutableReducerboth should be functions.');", + "assert((function() { const initialState = store.getState(); const isFrozen = DeepFreeze(initialState); store.dispatch(removeItem(3)); const state_1 = store.getState(); store.dispatch(removeItem(2)); const state_2 = store.getState(); store.dispatch(removeItem(0)); store.dispatch(removeItem(0)); store.dispatch(removeItem(0)); const state_3 = store.getState(); return isFrozen && DeepEqual(state_1, [0, 1, 2, 4, 5]) && DeepEqual(state_2, [0, 1, 4, 5]) && DeepEqual(state_3, [5]); })(), 'message: Dispatching theremoveItemaction creator should remove items from the state and should NOT mutate state.');" + ], + "solutions": [ + "const immutableReducer = (state = [0,1,2,3,4,5], action) => {\n switch(action.type) {\n case 'REMOVE_ITEM':\n return [\n ...state.slice(0, action.index),\n ...state.slice(action.index + 1)\n ];\n default:\n return state;\n }\n};\n\nconst removeItem = (index) => {\n return {\n type: 'REMOVE_ITEM',\n index\n }\n}\n\nconst store = Redux.createStore(immutableReducer);" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true + }, + { + "id": "5a24c314108439a4d403615b", + "title": "Copy an Object with Object.assign", + "releasedOn": "December 25, 2017", + "description": [ + "The last several challenges worked with arrays, but there are ways to help enforce state immutability when state is anobject, too. A useful tool for handling objects is theObject.assign()utility.Object.assign()takes a target object and source objects and maps properties from the source objects to the target object. Any matching properties are overwritten by properties in the source objects. This behavior is commonly used to make shallow copies of objects by passing an empty object as the first argument followed by the object(s) you want to copy. Here's an example:", + "const newObject = Object.assign({}, obj1, obj2);", + "This createsnewObjectas a newobject, which contains the properties that currently exist inobj1andobj2.", + "
", + "The Redux state and actions were modified to handle anobjectfor thestate. Edit the code to return a newstateobject for actions with typeONLINE, which set thestatusproperty to the stringonline. Try to useObject.assign()to complete the challenge." + ], + "files": { + "indexjsx": { + "key": "indexjsx", + "ext": "jsx", + "name": "index", + "contents": [ + "const defaultState = {", + " user: 'CamperBot',", + " status: 'offline',", + " friends: '732,982',", + " community: 'freeCodeCamp'", + "};", + "", + "const immutableReducer = (state = defaultState, action) => {", + " switch(action.type) {", + " case 'ONLINE':", + " // don't mutate state here or the tests will fail", + " return", + " default:", + " return state;", + " }", + "};", + "", + "const wakeUp = () => {", + " return {", + " type: 'ONLINE'", + " }", + "};", + "", + "const store = Redux.createStore(immutableReducer);" + ] + } + }, + "tests": [ + "assert((function() { const expectedState = { user: 'CamperBot', status: 'offline', friends: '732,982', community: 'freeCodeCamp' }; const initialState = store.getState(); return DeepEqual(expectedState, initialState); })(), 'message: The Redux store should exist and initialize with a state that is equivalent to thedefaultStateobject declared on line 1.');", + "assert(typeof wakeUp === 'function' && typeof immutableReducer === 'function', 'message:wakeUpandimmutableReducerboth should be functions.');", + "assert((function() { const initialState = store.getState(); const isFrozen = DeepFreeze(initialState); store.dispatch({type: 'ONLINE'}); const finalState = store.getState(); const expectedState = { user: 'CamperBot', status: 'online', friends: '732,982', community: 'freeCodeCamp' }; return isFrozen && DeepEqual(finalState, expectedState); })(), 'message: Dispatching an action of typeONLINEshould update the propertystatusin state toonlineand should NOT mutate state.');", + "getUserInput => assert(getUserInput('index').includes('Object.assign'), 'message:Object.assignshould be used to return new state.');" + ], + "solutions": [ + "const defaultState = {\n user: 'CamperBot',\n status: 'offline',\n friends: '732,982',\n community: 'freeCodeCamp'\n};\n\nconst immutableReducer = (state = defaultState, action) => {\n switch(action.type) {\n case 'ONLINE':\n return Object.assign({}, state, {\n status: 'online'\n });\n default:\n return state;\n }\n};\n\nconst wakeUp = () => {\n return {\n type: 'ONLINE'\n }\n};\n\nconst store = Redux.createStore(immutableReducer);" + ], + "type": "modern", + "isRequired": false, + "translations": {}, + "redux": true } ] -} \ No newline at end of file +} diff --git a/seed/index.js b/seed/index.js index e1d7f79a07b..03f5f86167e 100644 --- a/seed/index.js +++ b/seed/index.js @@ -45,6 +45,7 @@ Observable.combineLatest( var isLocked = !!challengeSpec.isLocked; var message = challengeSpec.message; var required = challengeSpec.required || []; + var template = challengeSpec.template; console.log('parsed %s successfully', blockName); @@ -113,6 +114,7 @@ Observable.combineLatest( }) .join(' '); challenge.required = (challenge.required || []).concat(required); + challenge.template = challenge.template || template; return challenge; }); diff --git a/seed/test-challenges.js b/seed/test-challenges.js index dfb0eb864ec..eb628a991e8 100644 --- a/seed/test-challenges.js +++ b/seed/test-challenges.js @@ -4,7 +4,6 @@ import { Observable } from 'rx'; import tape from 'tape'; import getChallenges from './getChallenges'; - function createIsAssert(t, isThing) { const { assert } = t; return function() { @@ -56,10 +55,21 @@ function createTest({ tests = [], solutions = [], head = [], - tail = [] + tail = [], + react = false, + redux = false, + reactRedux = false }) { solutions = solutions.filter(solution => !!solution); tests = tests.filter(test => !!test); + + // No support for async tests + const isAsync = s => s.includes('(async () => '); + if (isAsync(tests.join(''))) { + console.log(`Replacing Async Tests for Challenge ${title}`); + tests = tests.map(t => isAsync(t) ? "assert(true, 'message: great');" : t); + } + head = head.join('\n'); tail = tail.join('\n'); const plan = tests.length; @@ -81,16 +91,92 @@ function createTest({ }); } - return Observable.just(t) .map(fillAssert) /* eslint-disable no-unused-vars */ // assert and code used within the eval .doOnNext(assert => { solutions.forEach(solution => { + // Original code string + const originalCode = solution; tests.forEach(test => { - const code = solution; - const editor = { getValue() { return code; } }; + let code = solution; + + /* NOTE: Provide dependencies for React/Redux challenges + * and configure testing environment + */ + + let React, + ReactDOM, + Redux, + ReduxThunk, + ReactRedux, + Enzyme, + document; + + // Fake Deep Equal dependency + const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); + + // Hardcode Deep Freeze dependency + const DeepFreeze = (o) => { + Object.freeze(o); + Object.getOwnPropertyNames(o).forEach(function(prop) { + if (o.hasOwnProperty(prop) + && o[prop] !== null + && ( + typeof o[prop] === 'object' || + typeof o[prop] === 'function' + ) + && !Object.isFrozen(o[prop])) { + DeepFreeze(o[prop]); + } + }); + return o; + }; + + if (react || redux || reactRedux) { + // Provide dependencies, just provide all of them + React = require('react'); + ReactDOM = require('react-dom'); + Redux = require('redux'); + ReduxThunk = require('redux-thunk'); + ReactRedux = require('react-redux'); + Enzyme = require('enzyme'); + const Adapter15 = require('enzyme-adapter-react-15'); + Enzyme.configure({ adapter: new Adapter15() }); + + /* Transpile ALL the code + * (we may use JSX in head or tail or tests, too): */ + const transform = require('babel-standalone').transform; + const options = { presets: [ 'es2015', 'react' ] }; + + head = transform(head, options).code; + solution = transform(solution, options).code; + tail = transform(tail, options).code; + test = transform(test, options).code; + + const { JSDOM } = require('jsdom'); + // Mock DOM document for ReactDOM.render method + const jsdom = new JSDOM(` + + + + + + `); + const { window } = jsdom; + + // Mock DOM for ReactDOM tests + document = window.document; + global.window = window; + global.document = window.document; + + } + + const editor = { + getValue() { return code; }, + getOriginalCode() { return originalCode; } + }; /* eslint-enable no-unused-vars */ try { (() => { @@ -98,7 +184,8 @@ function createTest({ head + '\n;;' + solution + '\n;;' + tail + '\n;;' + - test); + test + ); })(); } catch (e) { t.fail(e); @@ -114,6 +201,7 @@ Observable.from(getChallenges()) .flatMap(challengeSpec => { return Observable.from(challengeSpec.challenges); }) + .filter(({ type }) => type !== 'modern') .flatMap(challenge => { return createTest(challenge); }) @@ -135,4 +223,3 @@ Observable.from(getChallenges()) err => { throw err; }, () => process.exit(0) ); -