在本章中,我们将通过构建应用中最复杂的组件,即Collection
组件的子组件,将您迄今为止学到的关于 React 组件的所有知识付诸实践。本章中我们的目标是获得扎实的反应经验并培养我们的反应肌肉。让我们开始吧!
如您所知,我们的Collection
组件有两个子组件:CollectionControls
和TweetList
。
我们将首先构建TweetList
组件。创建以下~/snapterest/source/components/TweetList.js
文件:
import React, { Component } from 'react';
import Tweet from './Tweet';
import TweetUtils from '../utils/TweetUtils';
const listStyle = {
padding: '0'
};
const listItemStyle = {
display: 'inline-block',
listStyle: 'none'
};
class TweetList extends Component {
getTweetElement = (tweetId) => {
const { tweets, onRemoveTweetFromCollection } = this.props;
const tweet = tweets[tweetId];
let tweetElement;
if (onRemoveTweetFromCollection) {
tweetElement = (
<Tweet
tweet={tweet}
onImageClick={onRemoveTweetFromCollection}
/>
);
} else {
tweetElement = <Tweet tweet={tweet}/>;
}
return (
<li style={listItemStyle} key={tweet.id}>
{tweetElement}
</li>
);
}
render() {
const tweetElements = TweetUtils
.getListOfTweetIds()
.map(this.getTweetElement);
return (
<ul style={listStyle}>
{tweetElements}
</ul>
);
}
}
export default TweetList;
TweetList
组件呈现推文列表:
render() {
const tweetElements = TweetUtils
.getListOfTweetIds()
.map(this.getTweetElement);
return (
<ul style={listStyle}>
{tweetElements}
</ul>
);
}
首先,我们创建一个Tweet
元素列表:
const tweetElements = TweetUtils
.getListOfTweetIds()
.map(this.getTweetElement);
TweetUtils.getListOfTweetIds()
方法返回一个 tweet id 数组。
然后,对于该数组中的每个 tweetID,我们创建一个Tweet
组件。为此,我们将在 tweet id 数组中调用map()
方法,并将this.getTweetElement
方法作为参数传递:
getTweetElement = (tweetId) => {
const { tweets, onRemoveTweetFromCollection } = this.props;
const tweet = tweets[tweetId];
let tweetElement;
if (onRemoveTweetFromCollection) {
tweetElement = (
<Tweet
tweet={tweet}
onImageClick={onRemoveTweetFromCollection}
/>
);
} else {
tweetElement = <Tweet tweet={tweet} />;
}
return (
<li style={listItemStyle} key={tweet.id}>
{tweetElement}
</li>
);
}
getTweetElement()
方法返回一个包裹在<li>
元素中的Tweet
元素。我们已经知道,Tweet
组件有一个可选的onImageClick
属性。我们希望何时提供此可选属性,何时不提供?
有两种情况。在第一个场景中,用户将单击 tweet 图像以将其从 tweet 集合中删除。在这个场景中,我们的Tweet
组件将对click
事件做出反应,因此我们需要提供onImageClick
属性。在第二个场景中,用户将导出没有用户交互的 tweet 静态集合。在这种情况下,我们不需要提供onImageClick
属性。
这正是我们在getTweetElement()
方法中所做的:
const { tweets, onRemoveTweetFromCollection } = this.props;
const tweet = tweets[tweetId];
let tweetElement;
if (onRemoveTweetFromCollection) {
tweetElement = (
<Tweet
tweet={tweet}
onImageClick={onRemoveTweetFromCollection}
/>
);
} else {
tweetElement = <Tweet tweet={tweet}/>;
}
我们创建一个tweet
常量,用于存储一条 tweet,其 ID 由tweetId
参数提供。然后,我们创建一个常量来存储父Collection
组件传递的this.props.onRemoveTweetFromCollection
属性。
接下来,我们检查this.props.onRemoveTweetFromCollection
属性是否由Collection
组件提供。如果是,那么我们创建一个具有onImageClick
属性的Tweet
元素:
tweetElement = (
<Tweet
tweet={tweet}
onImageClick={onRemoveTweetFromCollection}
/>
);
如果没有提供,那么我们创建一个没有handleImageClick
属性的Tweet
元素:
tweetElement = <Tweet tweet={tweet} />;
我们在以下两种情况下使用TweetList
组件:
- 在
Collection
组件中呈现 tweet 集合时使用此组件。在这种情况下,提供了onRemoveTweetFromCollection
属性*。* - 在呈现表示
Collection
组件中 tweet 集合的 HTML 标记字符串时使用此组件。在这种情况下,不提供onRemoveTweetFromCollection
属性*。*
一旦我们创建了Tweet
元素并将其放入tweetElement
变量中,我们将返回具有内联样式的<li>
元素:
return (
<li style={listItemStyle} key={tweet.id}>
{tweetElement}
</li>
);
除了style
属性外,我们的<li>
元素还有key
属性。React 使用它来标识动态创建的每个子元素。我建议您在阅读更多关于动态儿童的内容 https://facebook.github.io/react/docs/lists-and-keys.html 。
这就是getTweetElement()
方法的工作原理。因此,TweetList
组件返回一个无序的Tweet
元素列表:
return (
<ul style={listStyle}>
{tweetElements}
</ul>
);
现在,既然您理解了Collection
组件呈现的内容,那么让我们讨论一下它的子组件。我们将从CollectionControls
开始。创建以下~/snapterest/source/components/CollectionControls.js
文件:
import React, { Component } from 'react';
import Header from './Header';
import Button from './Button';
import CollectionRenameForm from './CollectionRenameForm';
import CollectionExportForm from './CollectionExportForm';
class CollectionControls extends Component {
state = {
name: 'new',
isEditingName: false
};
getHeaderText = () => {
const { name } = this.state;
const { numberOfTweetsInCollection } = this.props;
let text = numberOfTweetsInCollection;
if (numberOfTweetsInCollection === 1) {
text = `${text} tweet in your`;
} else {
text = `${text} tweets in your`;
}
return (
<span>
{text} <strong>{name}</strong> collection
</span>
);
}
toggleEditCollectionName = () => {
this.setState(prevState => ({
isEditingName: !prevState.isEditingName
}));
}
setCollectionName = (name) => {
this.setState({
name,
isEditingName: false
});
}
render() {
const { name, isEditingName } = this.state;
const {
onRemoveAllTweetsFromCollection,
htmlMarkup
} = this.props;
if (isEditingName) {
return (
<CollectionRenameForm
name={name}
onChangeCollectionName={this.setCollectionName}
onCancelCollectionNameChange={this.toggleEditCollectionName}
/>
);
}
return (
<div>
<Header text={this.getHeaderText()}/>
<Button
label="Rename collection"
handleClick={this.toggleEditCollectionName}
/>
<Button
label="Empty collection"
handleClick={onRemoveAllTweetsFromCollection}
/>
<CollectionExportForm htmlMarkup={htmlMarkup} />
</div>
);
}
}
export default CollectionControls;
顾名思义,CollectionControls
组件呈现一个用户界面来控制集合。这些控件允许用户执行以下操作:
- 重命名集合
- 清空集合
- 导出集合
收藏有一个名称。默认情况下,此名称为new
,用户可以更改。集合名称显示在由CollectionControls
组件呈现的标题中。此组件是存储集合名称的最佳候选组件,由于更改名称将需要组件重新呈现,我们将在组件的状态对象中存储该名称:
state = {
name: 'new',
isEditingName: false
};
CollectionControls
组件可以呈现集合控制元素或表单来更改集合名称。用户可以在两者之间切换。我们需要一种表示这两种状态的方法,我们将使用isEditingName
属性来实现这一目的。默认情况下,isEditingName
设置为false
;因此,当安装CollectionControls
组件时,用户不会看到更改集合名称的表单。让我们来看一下它的方法:
render() {
const { name, isEditingName } = this.state;
const {
onRemoveAllTweetsFromCollection,
htmlMarkup
} = this.props;
if (isEditingName) {
return (
<CollectionRenameForm
name={name}
onChangeCollectionName={this.setCollectionName}
onCancelCollectionNameChange={this.toggleEditCollectionName}
/>
);
}
return (
<div>
<Header text={this.getHeaderText()}/>
<Button
label="Rename collection"
handleClick={this.toggleEditCollectionName}
/>
<Button
label="Empty collection"
handleClick={onRemoveAllTweetsFromCollection}
/>
<CollectionExportForm htmlMarkup={htmlMarkup}/>
</div>
);
}
首先,我们检查组件状态的this.state.isEditingName
属性是否设置为true
。如果是,则CollectionControls
组件返回呈现表单以更改集合名称的CollectionRenameForm
组件:
<CollectionRenameForm
name={name}
onChangeCollectionName={this.setCollectionName}
onCancelCollectionNameChange={this.toggleEditCollectionName}
/>
CollectionRenameForm
组件呈现一个表单来更改集合名称。它接收三个属性:
name
属性,该属性引用当前集合名称- 引用组件方法的
onChangeCollectionName
和onCancelCollectionNameChange
属性
我们将在本章后面实现CollectionRenameForm
组件。现在让我们来仔细研究一下。
setCollectionName = (name) => {
this.setState({
name,
isEditingName: false
});
}
setCollectionName()
方法更新集合的名称,并隐藏一个表单,通过更新组件的状态来编辑集合名称。当用户提交新集合名称时,我们将调用此方法。
现在,让我们来看看 Apple T0.方法:
toggleEditCollectionName = () => {
this.setState(prevState => ({
isEditingName: !prevState.isEditingName
}));
}
此方法通过使用!
运算符将isEditingName
属性设置为其当前布尔值的相反值来显示或隐藏集合的名称编辑表单。当用户点击重命名集合或取消按钮时,我们将调用此方法,即显示或隐藏集合名称更改表单。
如果CollectionControls
组件状态的this.state.isEditingName
属性设置为false
,则返回采集控件:
return (
<div>
<Header text={this.getHeaderText()}/>
<Button
label="Rename collection"
handleClick={this.toggleEditCollectionName}
/>
<Button
label="Empty collection"
handleClick={onRemoveAllTweetsFromCollection}
/>
<CollectionExportForm htmlMarkup={htmlMarkup}/>
</div>
);
我们将Header
组件、两个Button
组件和CollectionExportForm
组件包装在div
元素中。您已经熟悉上一章中的Header
组件。它接收一个引用字符串的text
属性。但是,在这种情况下,我们不直接传递字符串,而是调用this.getHeaderText()
函数:
<Header text={this.getHeaderText()} />
依次,this.getHeaderText()
返回一个字符串。让我们仔细研究一下这个方法:
getHeaderText = () => {
const { name } = this.state;
const { numberOfTweetsInCollection } = this.props;
let text = numberOfTweetsInCollection;
if (numberOfTweetsInCollection === 1) {
text = `${text} tweet in your`;
} else {
text = `${text} tweets in your`;
}
return (
<span>
{text} <strong>{name}</strong> collection
</span>
);
}
此方法根据集合中的 tweet 数量为标题生成字符串。此方法的重要特性是,它不仅返回字符串,而且返回封装该字符串的 React 元素树。首先,我们创建numberOfTweetsInCollection
常量。它存储一个集合中的 tweet 数量。然后我们创建一个text
变量,并在一个集合中为其分配若干 tweets。此时,text
变量存储一个整数值。我们的下一个任务是根据的整数值将正确的字符串连接到它:
- 如果
numberOfTweetsInCollection
是1
,那么我们需要连接' tweet in your'
- 否则,我们需要连接
' tweets in your'
创建标题字符串后,我们将返回以下元素:
return (
<span>
{text} <strong>{name}</strong> collection
</span>
);
封装在<span>
元素中的最后一个字符串由text
变量的值、集合名称和collection
关键字组成;考虑这个例子:
1 tweet in your new collection.
一旦getHeaderText()
方法返回该字符串,它将作为属性传递给Header
组件。我们在CollectionControls
组件render()
方法中的下一个采集控制元素是Button
:
<Button
label="Rename collection"
handleClick={this.toggleEditCollectionName}
/>
我们将Rename collection
字符串传递给它的label
属性,this.toggleEditCollectionName
方法传递给它的handleClick
属性。因此,此按钮将具有Rename collection
标签,并将切换表单以更改集合名称。
下一个采集控制元素是我们的第二个Button
组件:
<Button
label="Empty collection"
handleClick={onRemoveAllTweetsFromCollection}
/>
正如您所猜测的,它将有一个Empty collection
标签,并且它将从一个集合中删除所有推文。
我们的最终收集控制元素为CollectionExportForm
:
<CollectionExportForm htmlMarkup={htmlMarkup} />
此元素接收表示集合的 HTML 标记字符串,并将呈现一个按钮。我们将在本章后面创建此组件。
现在,由于您了解了组件 To0T0 将呈现什么,让我们更深入地研究它的子组件中的 T2。我们将从CollectionRenameForm
组件开始。
首先,让我们创建这个~/snapterest/source/components/CollectionRenameForm.js
文件:
import React, { Component } from 'react';
import Header from './Header';
import Button from './Button';
const inputStyle = {
marginRight: '5px'
};
class CollectionRenameForm extends Component {
constructor(props) {
super(props);
const { name } = props;
this.state = {
inputValue: name
};
}
setInputValue = (inputValue) => {
this.setState({
inputValue
});
}
handleInputValueChange = (event) => {
const inputValue = event.target.value;
this.setInputValue(inputValue);
}
handleFormSubmit = (event) => {
event.preventDefault();
const { onChangeCollectionName } = this.props;
const { inputValue: collectionName } = this.state;
onChangeCollectionName(collectionName);
}
handleFormCancel = (event) => {
event.preventDefault();
const {
name: collectionName,
onCancelCollectionNameChange
} = this.props;
this.setInputValue(collectionName);
onCancelCollectionNameChange();
}
componentDidMount() {
this.collectionNameInput.focus();
}
render() {
const { inputValue } = this.state;
return (
<form className="form-inline" onSubmit={this.handleSubmit}>
<Header text="Collection name:"/>
<div className="form-group">
<input
className="form-control"
style={inputStyle}
onChange={this.handleInputValueChange}
value={inputValue}
ref={input => { this.collectionNameInput = input; }}
/>
</div>
<Button
label="Change"
handleClick={this.handleFormSubmit}
/>
<Button
label="Cancel"
handleClick={this.handleFormCancel}
/>
</form>
);
}
}
export default CollectionRenameForm;
此组件呈现一个表单以更改集合名称:
render() {
const { inputValue } = this.state;
return (
<form className="form-inline" onSubmit={this.handleSubmit}>
<Header text="Collection name:"/>
<div className="form-group">
<input
className="form-control"
style={inputStyle}
onChange={this.handleInputValueChange}
value={inputValue}
ref={input => this.collectionNameInput = input}
/>
</div>
<Button
label="Change"
handleClick={this.handleFormSubmit}
/>
<Button
label="Cancel"
handleClick={this.handleFormCancel}
/>
</form>
);
}
我们的<form>
元素包含四个元素,如下所示:
- 一个
Header
组件 - 一个
<input>
元素 - 两个
Button
组件
Header
组件呈现"Collection name:"
字符串。<input>
元素被包装在<div>
元素中,其className
属性设置为form-group
。这个名称是我们在第 5 章中讨论的引导框架的一部分,使您的 React 组件反应。它用于布局和样式设置,而不是 React 应用逻辑的一部分。
<input>
元素有很多属性。让我们仔细研究一下:
<input
className="form-control"
style={inputStyle}
onChange={this.handleInputValueChange}
value={inputValue}
ref={input => { this.collectionNameInput = input; }}
/>
以下是对上述代码中使用的属性的描述:
className
属性设置为form-control
。它是另一个类名,是引导框架的一部分。我们将使用它来设计样式。- 此外,我们使用
style
属性将自己的样式应用于这个input
元素,该属性使用单一样式规则引用inputStyle
对象,即marginRight
。 value
属性设置为组件状态this.state.inputValue
中存储的当前值。onChange
属性引用的handleInputValueChange
方法是onchange
事件处理程序。ref
属性是一种特殊的 React 属性,可以附加到任何组件。它采用回调函数,该函数将在安装和卸载组件后立即执行。它允许我们访问 React 组件呈现的 DOMinput
元素。
我希望您关注最后三个属性:value
、onChange
和ref
。value
属性设置为组件状态的属性,更改该值的唯一方法是更新其状态。另一方面,我们知道用户可以与输入字段交互并更改其值。这种行为是否适用于我们的组件?不会。每当用户键入时,我们输入字段的值都不会更改。这是因为组件控制着<input>
,而不是用户。在我们的CollectionRenameForm
组件中,<input>
的值始终反映this.state.inputValue
属性的值,无论用户类型如何。用户不受控制,但CollectionRenameForm
组件受控制。
那么,我们如何确保我们的输入字段对用户输入做出反应呢?我们需要监听用户输入,并更新CollectionRenameForm
组件的状态,这反过来将使用更新的值重新呈现输入字段。在每个输入的change
事件上这样做将使我们的输入看起来像往常一样工作,用户可以自由更改其值。
为此,我们为<input>
元素提供了引用组件this.handleInputValueChange
方法的onChange
属性:
handleInputValueChange = (event) => {
const inputValue = event.target.value;
this.setInputValue(inputValue);
}
正如我们在第 4 章中所讨论的,创建第一个 React 组件,React 将SyntheticEvent
的实例传递给事件处理程序。handleInputValueChange()
方法接收具有target
属性且具有value
属性的event
对象。此value
属性存储用户在输入字段中键入的字符串。我们将该字符串传递到我们的this.setInputValue()
方法中:
setInputValue = (inputValue) => {
this.setState({
inputValue
});
}
setInputValue()
方法是一种使用新输入值更新组件状态的方便方法。反过来,此更新将使用更新的值重新呈现<input>
元素。
安装CollectionRenameForm
组件时,初始输入值是多少?让我们来看看这个:
constructor(props) {
super(props);
const { name } = props;
this.state = {
inputValue: name
};
}
如您所见,我们从父组件传递集合的名称,并使用它设置组件的初始状态。
挂载此组件后,我们希望在输入字段上设置焦点,以便用户可以立即开始编辑集合的名称。我们知道,一旦一个组件被插入到 DOM 中,React 就会调用它的componentDidMount()
方法。此方法是我们设置focus
的最佳时机:
componentDidMount() {
this.collectionNameInput.focus();
}
为此,我们通过引用this.collectionNameInput
来获取输入元素,并对其调用focus()
函数。
我们如何在componentDidMount()
方法中引用 DOM 元素?请记住,我们为我们的input
元素提供了ref
属性。然后,我们将回调函数传递给该ref
属性,该属性又将 DOM 输入元素的引用分配给this.collectionNameInput
。现在我们可以通过访问this.collectionNameInput
属性来获取该引用。
最后,让我们讨论两个表单按钮:
Change
按钮提交表单并更改集合名称Cancel
按钮提交表单,但不更改集合名称
我们将从一个Change
按钮开始:
<Button
label="Change"
handleClick={this.handleFormSubmit}
/>
当用户点击时,调用this.handleFormSubmit
方法:
handleFormSubmit = (event) => {
event.preventDefault();
const { onChangeCollectionName } = this.props;
const { inputValue: collectionName } = this.state;
onChangeCollectionName(collectionName);
}
我们取消submit
事件,然后从组件的状态获取集合名称,并将其传递给this.props.onChangeCollectionName()
函数调用。onChangeCollectionName
函数由父CollectionControls
组件传递。调用此函数将更改集合的名称。
现在让我们讨论第二个表单按钮:
<Button
label="Cancel"
handleClick={this.handleFormCancel}
/>
当用户点击时,调用this.handleFormCancel
方法:
handleFormCancel = (event) => {
event.preventDefault();
const {
name: collectionName,
onCancelCollectionNameChange
} = this.props;
this.setInputValue(collectionName);
onCancelCollectionNameChange();
}
再次,我们取消一个submit
事件,然后获取父CollectionControls
组件作为属性传递的原始集合名称,并将其传递给我们的this.setInputValue()
函数。然后,我们调用隐藏收集控件的this.props.onCancelCollectionNameChange()
函数。
那是我们的CollectionRenameForm
组件。接下来,让我们创建我们在CollectionRenameForm
组件中重复使用了两次的Button
组件。
创建以下~/snapterest/source/components/Button.js
文件:
import React from 'react';
const buttonStyle = {
margin: '10px 0'
};
const Button = ({ label, handleClick }) => (
<button
className="btn btn-default"
style={buttonStyle}
onClick={handleClick}
>
{label}
</button>
);
export default Button;
Button
组件呈现一个按钮。
注意,我们没有声明类,而是定义了一个名为Button
的简单函数。这是创建 React 组件的功能方式。事实上,当组件的目的纯粹是渲染一些用户界面元素时,无论是否使用任何道具,建议您使用这种方法。
您可以将这个简单的 React 组件视为一个“纯”函数,它以props
对象的形式接受输入,并一致地返回 JSX 作为输出,无论您调用该函数多少次。
理想情况下,大多数组件都应该以“纯”JavaScript 函数的方式创建。当然,当您的组件具有状态时,这是不可能的,但是对于所有无状态组件来说,这是一个机会!现在看看我们迄今为止创建的所有组件,看看是否可以将它们重写为“纯”函数,而不是使用类。
我建议您在上阅读更多关于功能组件和类组件的信息 https://facebook.github.io/r
您可能想知道,如果您可以只使用<button>
元素,那么为按钮创建专用组件的好处是什么?将组件视为<button>
元素的包装器以及它附带的其他东西。在我们的例子中,大多数<button>
元素具有相同的样式,因此将<button>
和样式对象封装在组件中并重用该组件是有意义的。因此,专用的Button
组件。它希望从父组件接收两个属性:
label
属性是按钮的标签handleClick
属性是一个回调函数,当用户单击此按钮时调用
现在,是时候创建我们的CollectionExportForm
组件了。
CollectionExportForm
组件负责将收藏导出到第三方网站(http://codepen.io 。一旦你的收藏在 CodePen 上,你就可以保存它并与你的朋友分享。让我们来看看如何做到这一点。
创建~/snapterest/source/components/CollectionExportForm.js
文件:
import React from 'react';
const formStyle = {
display: 'inline-block'
};
const CollectionExportForm = ({ htmlMarkup }) => (
<form
action="http://codepen.io/pen/define"
method="POST"
target="_blank"
style={formStyle}
>
<input type="hidden" name="data" value={htmlMarkup}/>
<button type="submit" className="btn btn-default">
Export as HTML
</button>
</form>
);
export default CollectionExportForm;
CollectionExportForm
组件使用<input>
和<button>
元素呈现表单。<input>
元素是隐藏的,其值设置为 HTML 标记字符串,父组件将作为htmlMarkup
属性传递给该字符串。<button>
元素是此表单中唯一用户可见的元素。当用户点击导出为 HTML按钮时,一个集合将提交给 CodePen,并在新窗口中打开。然后,用户可以修改并共享该集合。
祝贺至此,您已经使用 React 构建了一个功能齐全的 web 应用。让我们看看它是如何工作的。
首先,确保我们在第 2 章为您的项目安装强大工具中安装和配置的 Snapkite 引擎正在运行。导航到~/snapkite-engine/
并运行以下命令:
npm start
然后,打开一个新的终端窗口,导航到~/snapterest/
,并运行以下命令:
npm start
现在在您的 web 浏览器中打开~/snapterest/build/index.html
。您将看到新的推文出现。单击它们以将其添加到您的收藏中。再次单击它们以从集合中删除单个 tweet。点击清空收藏按钮删除收藏中的所有推文。点击重命名收藏按钮,输入新收藏名称,点击更改按钮。最后,点击导出为 HTML按钮,将您的收藏导出到CodePen.io。如果您对本章或之前的章节有任何疑问,请转至https://github.com/fedosejev/react-essentials 并创建一个新的问题。
在本章中,您创建了TweetList
、CollectionControls
、CollectionRenameForm
、CollectionExportForm
和Button
组件。您已完成构建一个功能齐全的 React 应用。
在下一章中,我们将使用 Jest 测试此应用,并使用 Flux 和 Redux 对其进行增强。