联系人详情
前面已经完成了联系人列表,然而似乎与 Cordova 没啥关系,随便找个 Web 服务器就可以跑。那么标题上的 Cordova 就只是一个简单的 Web 窗口而已么?
当然不是,目前我们的数据是保存在 json 文件中的,以后会放在数据库中,以便于实现数据的增删改。数据库选用 SQLite,操作 SQLite 必须使用 Android 提供的一些 API,这时候就需要通过 Cordova 的插件来实现了。甚至再往后添加二维码等功能,都需要用到 Cordova。但是 Cordova 的 API 参与了开发之后,调试时就要部署在手机上进行了,操作多有不变,所以先处理前端不需要 Cordova 的部分,待前端成熟之后再来连接 Cordova。
详情页面
联系人列表完成之后,现在开始考虑详情页面。之所以需要详情页面,是因为还有一些不便于在列表中展示的信息(列表项显示范围有限),所以再在测试数据中添加更多的字段,比如性别、城市等
规划
详情页面写在 detail.html 中,对应的在 js 目录中创建一个 detail.jsx 保存 React 组件和脚本。样式表暂时仍然使用 index.css,在里面添加详情页面所需要的样式。所以新的 www 目录结构会是这样
详情页面仍然需要显示一个页头,标题就是姓名。之后的内容仍然用列表显示。用 HTML 表示大概会像这样
<ul>
<li>
<span class="label">姓名</span>
<span>张三</span>
</li>
<li>
<span class="label">电话</span>
<span>13801234567</span>
</li>
<li>
<span class="label">性别</span>
<span>男</span>
</li>
<li>
<span class="label">城市</span>
<span>四川省绵阳市</span>
</li>
</ul>
代码
detail.html
detail.html 的代码和 index.html 几乎一样,唯一的区别是
<script type="text/jsx" src="js/index.jsx"></script>
换成了
<script type="text/jsx" src="js/detail.jsx"></script>
detail.jsx
detail.jsx 中主要定义 3 个组件
- Detail 组件渲染联系人信息详情列表
- DetailItem 组件渲染联系某一项信息的内容
- Page 组件渲染页面内容,包括页头和 Detail 组件等
因为是静态页面,不能处理 GET 或 POST 参数,所以由地址栏的 HASH 传递联系人的 ID 参数,再在页面中通过 AJAX 获取数据,筛选出该详情页面需要显示的联系人信息。而数据获取就由 Page 组件处理(处理过程参考 index.jsx 中的 Page 组件。
之后由 Page 通过属性方式向 Detail 传递联系人数据;而 Detail 则拆分数据项,仍然通过 props 方式,逐项向 DetailItem 传递数据。
在容错方面,为了简化处理过程,如果没能取得联系人数据,则用一个姓名叫“查无此人”的默认的联系人数据代替。
定义 Detail 组件
var Detail = React.createClass({
render: function() {
var person = this.props.person;
return (
<A.List data-id={person.id}>
<DetailItem label="姓名" value={person.name} />
<DetailItem label="电话" value={person.tel} />
<DetailItem label="性别" value={person.isMan ? "男" : "女"} />
<DetailItem label="城市" value={person.city} />
</A.List>
);
}
});
注意,这里在 <A.List>
中传入了一个 data-id
属性,这个属性在作为 React 对象属性的同时,也会以标签属性形式渲染到 HTML 中——React 会把 data-
前缀的属性直接渲染为 HTML 标签属性。
DetailItem 组件
var DetailItem = React.createClass({
render: function() {
return (
<A.ListItem>
<span className="label">{this.props.label}</span>
<span>{this.props.value}</span>
</A.ListItem>
);
}
});
这个组件没什么悬念,只是直接把 label 的文本显示出来而已。
Page 组件
var Page = React.createClass({
defaultPerson: {
id: "0000",
name: "查无此人",
phone: "00000000000"
},
getInitialState: function() {
return {
person: null
};
},
componentDidMount: function() {
$.getJSON("/js/data.json").then(function(data) {
if (this.isMounted()) {
this.setState({
person: data.filter(function(p) {
return "#" + p.id === window.location.hash;
})[0]
});
}
}.bind(this));
},
render: function() {
var person = this.state.person || this.defaultPerson;
return (
<div>
<A.Header title={person.name} />
<Detail person={person} />
</div>
);
}
});
Page 组件和之前列表页面的 Page 组件一样,在 componentDidMount()
中通过 AJAX 加载数据。之后,通过 window.location.hash
按 ID 精确查找联系人,设置到 state 中。
显示的时候,如果没有找到 this.state.person
,则使用默认的“查无此人”联系人信息。
渲染 Page
最后当然不能忘了渲染根组件:Page。
React.render(<Page />, document.body);
这时候,通过 http://localhost/detail.html#1001
这个地址,已经可以看到数据显示出来,只不过由于没有添加样式,还不够美观。
在列表页面上添加到详情页面的连接
按照 Amazi UI React 文档,在列表页面上添加到详情页面的连接,只需要在 Person 组件中为 <A.ListItem>
添加 href
属性即可,
<A.ListItem className="person" href={"detail.html#" + this.props.id}>
</A.ListItem>
然后添加之后显示出来的效果却大大出乎意料。主要原因是 Amaze UI 将 li>a
的 display 设置为 block了,所以带链接的电话图标会显示在下一行。当然通过修改 CSS 是可以解决的,但是我想用另一种方法来解决:点击事件。
列表项上的点击事件
React 是支付事件处理的,在其 Event System 一章中说明了事件处理的方式和注意事项。React 可以处理的事件分几大类共计数十个,都在 Event System 中列举出来了。这里需要用到的是 onClick
事件。
注意,下面说的内容不是在 detail.jsx 而是在 index.jsx 中
首先为 Person 组件定义一个处理函数,用于处理列表项被点击后的动作:设置 window.location.href 跳转到详情页。
var Person = React.createClass({
handleClick: function(event) {
window.location.href = "detail.html#" + this.props.id;
},
render() { ... }
});
可以看到,数据来源仍然可以是 this.props
,同理,也可以是 this.state
。
之后在 <A.ListItem>
中绑定事件处理函数
render: function() {
var link = "tel:" + this.props.tel;
return (
<A.ListItem className="person" onClick={this.handleClick}>
...
</A.ListItem>
);
}
注意到 onClick={this.handleClick}
的写法,联想到在 HTML 标签属性中绑定事件处理函数的情况,函数在执行时 this
指针会变为全局对象 (window)。这里是不是需要 bind(this)
呢?
不需要!
React 已经处理了 this 的问题,所以这里只需要简单绑定 this.handleClick
,而在 handleClick
中使用 this
就是当前组件对象,不会是全局对象。
如果有强迫症,可能还需要给 <A.ListItem>
加个内联样式:style={{ cursor: "pointer" }}
,不过我认为没必要,因为我们的目标是手机 App,指哪点哪,完全没有 hover 一说。
可点击的详情电话
要让电话可点击只需要加 <a href="tel:xxxxxx">
即可。但是在详情页,每个数据项都是这个结构:
<ListItem>
<span>{label}</span>
<span>{value}</span>
</ListItem>
如果要加链接,就会变成下面的结构
<ListItem>
<span>{label}></span>
<span>
<a href={href}>{value}</a>
</span>
</ListItem>
然后再在 Detail 中使用组件的时候多传入一个 href
参数。
说起来,两个 DetailItem 中的区别只有第 2 个的 <span>
中的内容那一点,有没有办法可以重用呢?——可以试试 mixins,因为从 Reusable Components 的理解,mixins 有点像继承。
尝试(第 1 次失败)
var DetailItem = React.createClass({
getValueContent: function() {
return this.props.value;
},
render: function() {
return (
<A.ListItem className="person-detail-item">
<span className="label">{this.props.label}</span>
<span>{this.getValueContent()}</span>
</A.ListItem>
);
}
});
var DetailLinkItem = React.createClass({
mixins: [DetailItem],
getValueContent: function() {
return <a href={this.props.href}>{this.props.value}</a>;
}
});
结果失败了。再次阅读 Reusable Components 中的示例,然后发现 mixins 数组中应该是一个原型对象更贴切,尝试
第 2 次尝试(再次失败)
var DetailLinkItem = React.createClass({
mixins: [DetailItem.prototype],
getValueContent: function() {
return <a href={this.props.href}>{this.props.value}</a>;
}
});
这次是因为重复定义了 constructor。DetailItem.prototype
中有一个 constructor
,而 React.createClass()
又定义了一个。看来不能偷懒,只能定义个独立对象了
终于成功的尝试
var detailBase = {
render: function() {
return (
<A.ListItem className="person-detail-item">
<span className="label">{this.props.label}</span>
<span>{this.getValueContent()}</span>
</A.ListItem>
);
}
};
var DetailItem = React.createClass({
mixins: [detailBase],
getValueContent: function() {
return this.props.value
}
});
var DetailLinkItem = React.createClass({
mixins: [detailBase],
getValueContent: function() {
return <a href={this.props.href}>{this.props.value}</a>;
}
});
补充一下 Detail 中的变更
这个变更是在第 1 次尝试的时候就修改了的,主要是处理“电话”那一项时用 <DetailLinkItem>
代替了 <DetailItem>
。
var Detail = React.createClass({
render: function() {
var person = this.props.person;
return (
<A.List className="person-detail" data-id={person.id}>
<DetailItem label="姓名" value={person.name} />
<DetailLinkItem label="电话" value={person.tel} href={"tel:" + person.tel} />
<DetailItem label="性别" value={person.isMan ? "男" : "女"} />
<DetailItem label="城市" value={person.city} />
</A.List>
);
}
});
美化 Detail
美化通常放在最后,但不代表不需要,现在来加 className 和 CSS。
从效果上来看,主要是需要把 label 和后面的内容区分开来,顺便参照列表页面的列表样式,做一些细节上的美化。
先为 <A.List>
添加 className="person-detail"
,再为 <A.ListItem>
添加 className="person-detail-item"
。className="label"
是有先见之明早就加了的。最后修改 CSS,去掉 li
的类限定,再加上 ul.person-detail
和 li.person-detail-item
的样式
ul.person-list {
margin-top: 0;
}
li {
padding: 3px 6px;
}
li>.person-icon {
margin-right: 6px;
}
li>.person-phone {
margin-top: 4px;
}
ul.person-detail li {
margin-bottom: 4px;
border-top: 0;
}
li.person-detail-item .label {
margin-right: 8px;
padding: 2px 5px;
background-color: #41afff;
border-radius: 3px;
color: white;
}
最终效果还是比较令人满意的
更多建议: