在本章中,我们将通过在我们的应用中构建最复杂的组件,即 Collection
组件的子组件,将你迄今为止所学到的关于 React 组件的一切知识付诸实践。我们在本章中的目标是获得坚实的 React 经验并健壮我们的 React 肌肉。让我们开始吧!
创建 TweetList 组件
正如你所知道的,我们的 Collection
组件有两个子组件:CollectionControls
和 TweetList
。
我们将首先创建 TweetList
组件。新建以下 ~/snapterest/source/components/TweetList.js
文件:
import React, { Component } from 'react';
import Tweet from './Tweet';
const listStyle = {
padding: '0',
};
const listItemStyle = {
display: 'inline-block',
listStyle: 'none',
};
class TweetList extends Component {
getListOfTweetIds = () => Object.keys(this.props.tweets);
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 = this.getListOfTweetIds().map(this.getTweetElement);
return <ul style={listStyle}>{tweetElements}</ul>;
}
}
export default TweetList;
TweetList
组件渲染推文列表:
render() {
const tweetElements = this
.getListOfTweetIds()
.map(this.getTweetElement);
return (
<ul style={listStyle}>
{tweetElements}
</ul>
);
}
首先,我们创建一个 tweetElement
列表:
const tweetElements = this.getListOfTweetIds().map(this.getTweetElement);
getListOfTweetIds()
方法返回一个推文 ID 数组。
然后,对于该数组中的每个推文 ID,我们创建一个 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
组件将对单击事件做出响应,因此我们需要提供 onImageClick
属性。在第二个场景中,用户将导出一个没有用户交互的静态推文集合。在这种情况下,我们不需要提供 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
常量,该常量存储一个 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
组件中渲染推文集合。在本例中,提供了onRemoveTweetFromCollection
属性。 - 当渲染表示
Collection
组件中推文集合的 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>;
创建 CollectionControls 组件
现在,既然你了解了 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: 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
组件渲染用于控制集合的用户界面。这些控件允许用户执行以下操作:
- 重命名集合
- 清空集合
- 导出集合
集合具有名称。默认情况下,此名称是新的,用户可以更改它。集合名称显示在由 CollectionControls
组件渲染的标头中。该组件是存储集合名称的最佳候选组件,由于更改名称将需要组件重新渲染,因此我们将该名称存储在该组件的状态对象中:
state = {
name: 'new',
isEditingName: false,
};
CollectionControls
组件可以渲染集合控件元素或表单以更改集合名称。用户可以在两者之间切换。我们需要一种表示这两种状态的方法——为此我们将使用 isEditingName
属性。默认情况下,isEditingName
设置为 false
;因此在挂载 CollectionControl
s 组件时,用户不会看到用来更改集合名称的表单。让我们看看它的 render()
方法:
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
方法:
setCollectionName = (name) => {
this.setState({
name: name,
isEditingName: false,
});
};
setCollectionName()
方法更新集合的名称,并隐藏通过更新组件的状态来编辑集合名称的表单。当用户提交新的集合名称时,我们将调用该方法。
现在,让我们看看 toggleEditCollectionName()
方法:
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()
函数:
<Headertext={this.getHeaderText} />
相反的,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>
);
};
该方法根据集合中的推文数量为我们的标头生成一个字符串。该方法的重要特征是它不仅返回字符串,还返回封装该字符串的 React 元素树。首先,我们创建 numberOfTweetsInCollection
常量,它将存储推文集合的数量。然后,我们创建一个文本变量,并将推文集合的数量分配给它。此时,文本变量存储一个整数值。我们的下一个任务是根据整数值连接正确的字符串:
- 如果
numberOfTweetsInCollection
为1
,那么我们需要连接'tweet in your'
- 否则,我们需要连接
'tweets in your'
创建标头字符串后,我们将返回以下元素:
return (
<span>
{text} <strong>{name}</strong> collection
</span>
);
封装在 <span>
元素中的最终字符串由文本变量的值、集合名称和集合关键字组成;如以下示例:
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 标签字符串,并将渲染一个 Button。我们将在本章稍后创建该组件。
现在,既然你了解了 CollectionControls
组件将渲染的内容,那么让我们仔细看看它的子组件。我们将从 CollectionRenameForm
组件开始。
创建 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} />
Chapter 8 [ 119 ]
</form>
);
}
}
export default CollectionRenameForm;
该组件渲染一个更改集合名称的表单:
render() {
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={this.state.inputValue}
ref="collectionName" />
</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
。这个类名是 Bootstrap 框架的一部分,我们在第 5 章让你的 React 组件具有响应式中讨论过。它用于布局和样式,不是 React 应用逻辑的一部分。
<input>
元素有很多属性。让我们仔细看看:
<input className="form-control" style={inputStyle}
onChange={this.handleInputValueChange} value={inputValue} ref={(input) => {
this.collectionNameInput = input; }} />
以下是前面代码中使用的属性的说明:
className
属性设置为form-control
。它是另一个类名,是 Bootstrap 框架的一部分。我们将使用它进行样式设置。- 此外,我们使用
style
属性将自己的样式应用于此input
元素,该属性使用单个样式规则(即marginRight
)引用inputStyle
对象。 value
属性设置为存储在组件状态 this.state.inputValue 中的当前值。onChange
属性引用了一个handleInputValueChange
方法,该方法是一个onChange
事件处理程序。ref
属性是一个特殊的 React 属性,可以附加到任何组件。它需要一个回调函数,React 将在安装和卸载组件后立即执行执行该函数。它允许我们访问 React 组件渲染的 DOM 输入元素。
我希望你关注最后三个属性:value
、onChange
和 ref
。value
属性设置为组件状态的属性,更改该值的唯一方法是更新其状态。另一方面,我们知道用户可以与输入字段交互并更改其值。此行为是否适用于我们的组件呢?不。每当用户键入时,我们的输入字段的值都不会改变。这是因为组件控制 <input>
,而不是由用户控制。在 CollectionRenameForm
组件中,不管用户键入什么,<input>
的值始终反映 this.state.inputValue
属性值。用户不受控制,但 CollectionRenameForm
组件受控制。
那么,我们如何确保输入字段响应用户输入呢?我们需要监听用户输入,并更新 CollectionRenameForm
组件的状态,反过来将使用更新的值重新渲染输入字段。
对每个输入更改事件这样做将使我们的输入看起来像往常一样工作,用户可以随意更改其值。
为此,我们为 <input>
元素提供了 onChange
属性,该属性会引用组件的 this.handleInputValueChange
方法:
handleInputValueChange = (event) => {
const inputValue = event.target.value;
this.setInputValue(inputValue);
};
正如我们在第 4 章创建你的第一个 React 组件中所讨论的,React 将 SyntheticEvent
实例传递给事件处理程序。handleInputValueChange()
方法接收事件对象,该对象的 target
属性有 value
属性。此 value
属性存储用户在输入字段中键入的字符串。我们把那个字符串传给 this.setInputValue()
方法:
setInputValue = (inputValue) => {
this.setState({
inputValue,
});
};
setInputValue()
方法是一种使用新的输入值更新组件的状态的简便方法。相反的,此更新将使用更新值重新渲染 <input>
元素。
当挂载 CollectionRenameForm
组件时 <input>
的初始值时什么?让我们来看看:
constructor(props) {
super(props);
const { name } = props;
this.state = {
inputValue: name
};
}
如你所见,我们从父组件传递集合名称,并使用它设置组件的初始状态。
挂载该组件后,我们希望将焦点设置在输入字段上,以便用户可以立即开始编辑集合名称。我们知道,一旦将组件插入到 DOM 中,React 就会调用其 componentDidMount()
方法,这种方法是我们设定 focus
的最佳机会:
componentDidMount() {
this.collectionNameInput.focus();
}
要做到这一点,我们通过引用 this.collectionNameInput
来获得输入元素,并对其调用 focus()
函数。
我们如何在 componentDidMount()
方法中引用 DOM 元素?请记住,我们为输入元素提供了 ref
属性。然后我们将回调函数传递给该 ref
属性,该属性反过来又将 DOM 输入元素引用分配给 this.collectionNameInput
。所以现在我们可以通过访问 this.collectionNameInput
属性来访问该引用。
最后,让我们讨论两个表单按钮:
Change
按钮提交表单并更改集合名称Cancel
按钮提交表单,但不更改集合名称
我们将从 Change
按钮开始:
<button label="Change" handleClick="{this.handleFormSubmit}" />
当用户单击它时会调用 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}" />
当用户单击它时,handleFormCancel
方法被调用:
handleFormCancel = (event) => {
event.preventDefault();
const { name: collectionName, onCancelCollectionNameChange } = this.props;
this.setInputValue(collectionName);
onCancelCollectionNameChange();
};
我们再次取消提交事件,然后获取父 CollectionControls
组件作为属性传递的原始集合名称,并将其传递给 this.setInputValue()
函数。然后,我们调用 this.props.onCancelCollectionNameChange()
函数来隐藏集合控件。
这是我们的 CollectionRenameForm
组件。接下来,让我们在 CollectionRenameForm
组件中创建重复使用两次的 Button 组件。
创建 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 组件的功能方式。事实上,当组件的目的纯粹是渲染一些带有或不带有任何 props 的用户界面元素时,建议你使用这种方法。
你可以将这个简单的 React 组件视为一个“纯”函数,它接受 props 对象形式的输入,并始终返回 JSX 作为输出,无论你调用该函数多少次。
理想情况下,你的大多数组件都应该以“纯”JavaScript 函数的方式创建。当然,当你的组件有状态时,这是不可能的,但对于所有无状态组件,这是可能的!现在看看我们迄今为止创建的所有组件,看看是否可以将它们重写为“纯”函数而不是使用类。
我建议你通过以下链接阅读更多关于函数组件与类组件的信息:
https://facebook.github.io/react/docs/components-and-props.html
你可能想知道,如果你可以使用 <button>
元素,那么为按钮创建专用组件有什么好处呢?将组件视为 <button>
元素及其附带的其他元素的包装器。在我们的例子中,大多数 <button>
元素都具有相同的样式,因此将 <button>
和样式对象封装在组件中并重用该组件是有意义的。因此,专用的 Button
组件。它期望从父组件接收两个属性:
label
属性是按钮的标签handleClick
属性是用户单击此按钮时调用的回调函数
现在,是创建 CollectionExportForm
组件的时候了。
创建 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-engine 正在运行。转到 ~/snapkite-engine/
并运行以下命令:
npm start
然后,打开一个新的终端窗口,转到 ~/snapterest/
,并运行以下命令:
npm start
现在打开 ~/snapterest/build/index.html
。你将看到新的推文出现。单击它们可将它们添加到你的集合中。再次单击它们以从集合中删除单个推文。单击清空集合按钮,从集合中删除所有推文。单击重命名集合按钮,键入新集合名称,然后单击修改按钮。最后单击导出 HTML按钮,将集合导出到 CodePen.io。如果你对本章或前几章有任何问题,请转到https://github.com/PacktPublishing/React-Essentials-Second-Edition并新建问题。
总结
在本章中,你创建了 TweetList
、CollectionControls
、CollectionRenameForm
、CollectionExportForm
和 Button
组件。你已完成构建一个功能完备的 React 应用。
在接下来的章节中,我们将使用 Jest 测试这个应用,并使用 Flux 和 Redux 增强它。
评论 (0)