Webpack&React (七) 从便签到看板

渣翻译, 原文链接

从便签到看板


这里写图片描述

So far we have developed an application for keeping track of notes in localStorage. We still have work to do to turn this into a real Kanban as pictured above. Most importantly our system is missing the concept of Lane.

到目前为至,我们已经开发了一个应用可以写便签到localStorage.我们要把它变成如上图那样的真的Kanban应用,还有许多工作要做,最重要的是我们的系统现在没有这个概念.

A Lane is something that should be able to contain many Notes within itself and track their order. One way to model this is simply to make a Lane point at Notes through an array of Note ids. This relation could be reversed. A Note could point at a Lane using an id and maintain information about its position within a Lane. In this case, we are going to stick with the former design as that works well with re-ordering later on.

栏应该是能包含许些便签并跟踪它们.一种方式是使Lane使用Note的id数组指向一组Notes,同样可以反转关系,一个Note包含Lane的id和它自已的所处位置指向包含它的Lane。在这个例子中,我们使用前者。

提取Lane


As earlier, we can use the same idea of two components here. There will be a component for the higher level (i.e., Lanes) and for the lower level (i.e., Lane). The higher level component will deal with lane ordering. A Lane will render itself (i.e., name and Notes) and have basic manipulation operations.

在之前,我们使用相同的思想构建了两人个组件。将会有一个上级主件,例如Lanes,和一个下级组件,例如Lane。上级组件处理lane的排序,下级Lane将渲染它本身(例如,名字和所含Notes)并会有基本的操作。

Just as with Notes, we are going to need a set of actions. For now it is enough if we can just create new lanes so we can create a corresponding action for that as below:

正如同Notes,我们将需要设置一组行为,如果我们现在只需要创建新的栏目(lane),正如下面所做的就可以了:

app/actions/LaneActions.js

import alt from '../libs/alt';

export default alt.generateActions('create');

In addition, we are going to need a LaneStore and a method matching to create. The idea is pretty much the same as for NoteStore earlier. create will concatenate a new lane to the list of lanes. After that, the change will propagate to the listeners (i.e., FinalStore and components).

此外,我们需要一个LaneStore和一个在上面行为中声明的create方法。这个想法和之前的NoteStore是差不多的。create将新建一个新的lane到栏列表中,之后,所有改变都会传递到监听器中(例如,FinalStore和组件)

app/stores/LaneStore.js

import uuid from 'node-uuid';
import alt from '../libs/alt';
import LaneActions from '../actions/LaneActions';

class LaneStore {
  constructor() {
    this.bindActions(LaneActions);

    this.lanes = [];
  }
  create(lane) {
    const lanes = this.lanes;

    lane.id = uuid.v4();
    lane.notes = lane.notes || [];

    this.setState({
      lanes: lanes.concat(lane)
    });
  }
}

export default alt.createStore(LaneStore, 'LaneStore');

We are also going to need a stub for Lanes. We will expand this later. For now we just want something simple to show up.

我们还需要一个Lanes类,这个类会在以后进行扩展,现在我们只需要显示一些简单的东西。

app/components/Lanes.jsx

import React from 'react';

export default class Lanes extends React.Component {
  render() {
    return (
      <div className="lanes">
        lanes should go here
      </div>
    );
  }
}

Next, we need to make room for Lanes at App. We will simply replace Notes references with Lanes, set up actions, and store as needed:

下一步,我们要在App中放置Lanes,我们将只是用Lanes替换Notes,设置行为和存储。

app/components/App.jsx

import AltContainer from 'alt-container';
import React from 'react';
leanpub-start-delete
import Notes from './Notes.jsx';
import NoteActions from '../actions/NoteActions';
import NoteStore from '../stores/NoteStore';
leanpub-end-delete
leanpub-start-insert
import Lanes from './Lanes.jsx';
import LaneActions from '../actions/LaneActions';
import LaneStore from '../stores/LaneStore';
leanpub-end-insert

export default class App extends React.Component {
  render() {
    return (
      <div>
leanpub-start-delete
        <button className="add-note" onClick={this.addNote}>+</button>
leanpub-end-delete
leanpub-start-insert
        <button className="add-lane" onClick={this.addLane}>+</button>
leanpub-end-insert
leanpub-start-delete
        <AltContainer
          stores={[NoteStore]}
          inject={{
            notes: () => NoteStore.getState().notes
          }}
        >
leanpub-end-delete
leanpub-start-insert
        <AltContainer
          stores={[LaneStore]}
          inject={{
            lanes: () => LaneStore.getState().lanes || []
          }}
        >
leanpub-end-insert
leanpub-start-delete
          <Notes onEdit={this.editNote} onDelete={this.deleteNote} />
leanpub-end-delete
leanpub-start-insert
          <Lanes />
leanpub-end-insert
        </AltContainer>
      </div>
    );
  }
leanpub-start-delete
  deleteNote = (id) => {
    NoteActions.delete(id);
  };
  addNote = () => {
    NoteActions.create({task: 'New task'});
  };
  editNote = (id, task) => {
    // Don't modify if trying set an empty value
    if(!task.trim()) {
      return;
    }

    NoteActions.update({id, task});
  };
leanpub-end-delete
leanpub-start-insert
  addLane() {
    LaneActions.create({name: 'New lane'});
  }
leanpub-end-insert
}

If you check out the implementation at the browser, you can see that the current implementation doesn’t do much. It just shows a plus button and lanes should go here text. Even the add button doesn’t work yet. We still need to model Lane and attach Notes to that to make this all work.

如果现在你在浏览器中查看结果,你会发现并没有做太多东西,只是有一个增加按钮和lanes should go here这句话,按钮现在不好使。为了让这一切工作,我们需要建模Lane并给它附加Notes。

建模 Lane


The Lanes container will render each Lane separately. Each Lane in turn will then render associated Notes, just like our App did earlier. Lanes is analogous to Notes in this manner. The example below illustrates how to set this up:

Lanes容器将分别渲染每个Lane,而每个Lane会依次渲染与其相关的Notes,就如同我们App以前做的那样。Lanes与Notes是相似的。如下所示:

app/components/Lanes.jsx

import React from 'react';
import Lane from './Lane.jsx';

export default ({lanes}) => {
  return (
    <div className="lanes">{lanes.map(lane =>
      <Lane className="lane" key={lane.id} lane={lane} />
    )}</div>
  );
}

We are also going to need a Lane component to make this work. It will render the Lane name and associated Notes. The example below has been modeled largely after our earlier implementation of App. It will render an entire lane, including its name and associated notes:

我们还需要一个Lane组件。它将渲染一个Lane名字和它相关联的Notes,下面这个例子大量用了原来在App中的实现:

app/components/Lane.jsx

import AltContainer from 'alt-container';
import React from 'react';
import Notes from './Notes.jsx';
import NoteActions from '../actions/NoteActions';
import NoteStore from '../stores/NoteStore';

export default class Lane extends React.Component {
  render() {
    const {lane, ...props} = this.props;

    return (
      <div {...props}>
        <div className="lane-header">
          <div className="lane-add-note">
            <button onClick={this.addNote}>+</button>
          </div>
          <div className="lane-name">{lane.name}</div>
        </div>
        <AltContainer
          stores={[NoteStore]}
          inject={{
            notes: () => NoteStore.getState().notes || []
          }}
        >
          <Notes onEdit={this.editNote} onDelete={this.deleteNote} />
        </AltContainer>
      </div>
    );
  }
  editNote(id, task) {
    // Don't modify if trying set an empty value
    if(!task.trim()) {
      return;
    }

    NoteActions.update({id, task});
  }
  addNote() {
    NoteActions.create({task: 'New task'});
  }
  deleteNote(id) {
    NoteActions.delete(id);
  }
}

I am using Object rest/spread syntax (stage 2) (const {a, b, …props} = this.props) in the example. This allows us to attach a className to Lane and we avoid polluting it with HTML attributes we don’t need. The syntax expands Object key value pairs as props so we don’t have to write each prop we want separately.

在这个例子中使用了Object rest/spread syntax (stage 2),(const {a, b, ...props} = this.props),这让我们附带className到Lane并避免把其它我们不需要的属性带到HTML。这个语法使用键值对做为属性,所以我们不用分别写我们的每一个属性。

If you run the application and try adding new notes, you can see there’s something wrong. Every note you add is shared by all lanes. If a note is modified, other lanes update too.

如果你运行这个应用并增加一个新的notes,你会发现一新不对劲的地方,每个你增加的note被所有的lanes共用,一个note被修改,其它lanes同样被更新。

这里写图片描述

The reason why this happens is simple. Our NoteStore is a singleton. This means every component that is listening to NoteStore will receive the same data. We will need to resolve this problem somehow.

为什么会是这样是很好理解的,我们的NoteStore是单例的。所以每个监听到NoteStore的组件都引用相同的数据。我们将在下面解决这个问题。

Making Lanes Responsible of Notes


Currently, our Lane contains just an array of objects. Each of the objects knows its id and name. We’ll need something more.

当前,我们的Lane只包含了一个对象数组。每个对象都包含它的idname,我们需要更多的东西。

In order to make this work, each Lane needs to know which Notes belong to it. If a Lane contained an array of Note ids, it could then filter and display the Notes belonging to it. We’ll implement a scheme to achieve this next.

为了使这个工作,每个Lane需要知道哪些Notes属于它,所以Lane包含一个Note的id数组,它能过滤并显示属于它的Notes。下一步我们将实现一个方案达到这个效果。

设置attachToLane

When we add a new Note to the system using addNote, we need to make sure it’s associated to some Lane. This association can be modeled using a method, such as LaneActions.attachToLane({laneId: , noteId: }). Before calling this method we should create a note and gets its id. Here’s an example of how we could glue it together:

当我们通过addNote增加一个新的Note到系统中,我们需要确保它已经关联到某个Lane.这个关联能使用方法如:LaneActions.attachToLane({laneId: <id>, noteId: <id>})。调用这个方法之前,我们应该创建了一个Note并得到它的id。下面例子展示了怎么结合它们到一起:

const note = NoteActions.create({task: 'New task'});

LaneActions.attachToLane({
  noteId: note.id,
  laneId
});

To get started we should add attachToLane to actions as before:

开始,我们像以前一样增加attachToLane到我们的actions:

app/actions/LaneActions.js

import alt from '../libs/alt';

export default alt.generateActions('create', 'attachToLane');

In order to implement attachToLane, we need to find a lane matching to the given lane id and then attach note id to it. Furthermore, each note should belong only to one lane at a time. We can perform a rough check against that:

为了实现attachToLane,我们需要用给出的lane的id找到指定的lane然后附加note的id到它。此外,每个note应当只属于一个lane.我们能像这样执行一个粗略的检查:

app/stores/LaneStore.js

import uuid from 'node-uuid';
import alt from '../libs/alt';
import LaneActions from '../actions/LaneActions';

class LaneStore {
  ...
leanpub-start-insert
  attachToLane({laneId, noteId}) {
    const lanes = this.lanes.map(lane => {
      if(lane.id === laneId) {
        if(lane.notes.includes(noteId)) {
          console.warn('Already attached note to lane', lanes);
        }
        else {
          lane.notes.push(noteId);
        }
      }

      return lane;
    });

    this.setState({lanes});
  }
leanpub-end-insert
}

export default alt.createStore(LaneStore, 'LaneStore');

We also need to make sure NoteActions.create returns a note so the setup works just like in the code example above. The note is needed for creating an association between a lane and a note:

我们需要确保NoteActions.create反回一个note,这个note需要创建一个lane和note之间的关联。

app/stores/NoteStore.js

...

class NoteStore {
  constructor() {
    this.bindActions(NoteActions);

    this.notes = [];
  }
  create(note) {
    const notes = this.notes;

    note.id = uuid.v4();

    this.setState({
      notes: notes.concat(note)
    });

leanpub-start-insert
    return note;
leanpub-end-insert
  }
  ...
}

...

设置detachFromLane

deleteNote is the opposite operation of addNote. When removing a Note, it’s important to remove its association with a Lane as well. For this purpose we can implement LaneActions.detachFromLane({laneId: }). We would use it like this:

deleteNoteaddNote 相反的操作,当移除一个Note时同样要在Lane移除它。我们能使用LaneActions.detachFromLane({laneId: <id>}).我们像下面使用它:

LaneActions.detachFromLane({laneId, noteId});
NoteActions.delete(noteId);

Again, we should set up an action:
再次,我们需要设置一个行为:

app/actions/LaneActions.js

import alt from '../libs/alt';

export default alt.generateActions('create', 'attachToLane', 'detachFromLane');

The implementation will resemble attachToLane. In this case, we’ll remove the possibly found Note instead:

这个实现与attachToLane相似:

app/stores/LaneStore.js

import uuid from 'node-uuid';
import alt from '../libs/alt';
import LaneActions from '../actions/LaneActions';

class LaneStore {
  ...
  attachToLane({laneId, noteId}) {
    ...
  }
leanpub-start-insert
  detachFromLane({laneId, noteId}) {
    const lanes = this.lanes.map(lane => {
      if(lane.id === laneId) {
        lane.notes = lane.notes.filter(note => note !== noteId);
      }

      return lane;
    });

    this.setState({lanes});
  }
leanpub-end-insert
}

export default alt.createStore(LaneStore, 'LaneStore');

Just building an association between a lane and a note isn’t enough. We are going to need some way to resolve the note references to data we can display through the user interface. For this purpose, we need to implement a special getter so we get just the data we want per each lane.

实现NoteStore的Getter

One neat way to resolve lane notes to actual data is to implement a public method NoteStore.getNotesByIds(notes). It accepts an array of Note ids, and returns the corresponding objects.

查找出栏中的所有便签的一个简洁方式是实现一个NoteStore.getNotesByIds(notes),它接受一组note的id并反回相应的对象.

Just implementing the method isn’t enough. We also need to make it public. In Alt, this can be achieved using this.exportPublicMethods. It takes an object that describes the public interface of the store in question. Consider the implementation below:

只实现这个方法是不够的,还需要让这个方法为public的.使用Alt,可以通过this.exportPublicMethods达到这个目地,它接受一个对象描述这个公开的接口.考虑下面的实现:

app/stores/NoteStore.jsx

import uuid from 'node-uuid';
import alt from '../libs/alt';
import NoteActions from '../actions/NoteActions';

class NoteStore {
  constructor() {
    this.bindActions(NoteActions);

    this.notes = [];

leanpub-start-insert
    this.exportPublicMethods({
      getNotesByIds: this.getNotesByIds.bind(this)
    });
leanpub-end-insert
  }
  ...
leanpub-start-insert
  getNotesByIds(ids) {
    // 1. Make sure we are operating on an array and
    // map over the ids
    // [id, id, id, ...] -> [[Note], [], [Note], ...]
    return (ids || []).map(
      // 2. Extract matching notes
      // [Note, Note, Note] -> [Note, ...] (match) or [] (no match)
      id => this.notes.filter(note => note.id === id)
    // 3. Filter out possible empty arrays and get notes
    // [[Note], [], [Note]] -> [[Note], [Note]] -> [Note, Note]
    ).filter(a => a.length).map(a => a[0]);
  }
leanpub-end-insert
}

export default alt.createStore(NoteStore, 'NoteStore');

Note that the implementation filters possible non-matching ids from the result.

注意这个过滤后可能没有匹配到任何值.

连接Lane的逻辑

Now that we have the logical bits together, we can integrate it with Lane. We’ll need to take the newly added props (id, notes) into account, and glue this all together:

现在我们有一些逻辑,我们可以在Lane中整合它们,我们需要考虑最近增加的一些属性(id, notes).

app/components/Lane.jsx

...
leanpub-start-insert
import LaneActions from '../actions/LaneActions';
leanpub-end-insert

export default class Lane extends React.Component {
  render() {
    const {lane, ...props} = this.props;

    return (
      <div {...props}>
        <div className="lane-header">
          <div className="lane-add-note">
            <button onClick={this.addNote}>+</button>
          </div>
          <div className="lane-name">{lane.name}</div>
        </div>
        <AltContainer
          stores={[NoteStore]}
          inject={{
leanpub-start-delete
            notes: () => NoteStore.getState().notes || []
leanpub-end-delete
leanpub-start-insert
            notes: () => NoteStore.getNotesByIds(lane.notes)
leanpub-end-insert
          }}
        >
          <Notes onEdit={this.editNote} onDelete={this.deleteNote} />
        </AltContainer>
      </div>
    );
  }
  editNote(id, task) {
    // Don't modify if trying set an empty value
    if(!task.trim()) {
      return;
    }

    NoteActions.update({id, task});
  }
leanpub-start-delete
  addNote() {
    NoteActions.create({task: 'New task'});
  }
  deleteNote(id, e) {
    e.stopPropagation();

    NoteActions.delete(id);
  }
leanpub-end-delete
leanpub-start-insert
  addNote = (e) => {
    const laneId = this.props.lane.id;
    const note = NoteActions.create({task: 'New task'});

    LaneActions.attachToLane({
      noteId: note.id,
      laneId
    });
  };
  deleteNote = (noteId, e) => {
    e.stopPropagation();

    const laneId = this.props.lane.id;

    LaneActions.detachFromLane({laneId, noteId});
    NoteActions.delete(noteId);
  };
leanpub-end-insert
}

有三个重要改变:

  • Methods where we need to refer to this have been bound using a property initializer. An alternative way to achieve this would have been to bind at render or at constructor.
  • 这些方法需要绑定到this对象,我们使用属性初始化(箭头函数,lambda表达式).其它的方式可以在renderconstructor中使用bind达到这个目地.
  • notes: () => NoteStore.getNotesByIds(notes) - Our new getter is used to filter notes.
  • notes: () => NoteStore.getNotesByIds(notes) - 获取指定的的notes.
  • addNote, deleteNote - These operate now based on the new logic we specified. Note that we trigger detachFromLane before delete at deleteNote. Otherwise we may try to render non-existent notes. You can try swapping the order to see warnings.
  • addNote, deleteNote - 这些操作现在基于我们指定的新逻辑.注意我们要在deleteNote前,触发detachFromLane ,否则我们可能会渲染不存在的notes. 你可以试着交换一下顺序看一下警告信息.

After these changes, we have a system that can maintain relations between Lanes and Notes. The current structure allows us to keep singleton stores and a flat data structure. Dealing with references is a little awkward, but that’s consistent with the Flux architecture.

这样修改后,我们的程序能管理Lanes和Notes之间的关系.现在的这个结构让我们保持stores的单例和数据结构的扁平化.

If you try to add notes to a specific lane, they shouldn’t be duplicated anymore. Also editing a note should behave as you might expect:

如果你在指定的lane中增加notes,它应该不会重复,对于修改也是一样.

这里写图片描述

数据依赖和waitFor

The current setup works because our actions are synchronous. It would become more problematic if we dealt with a back-end. In that case, we would have to set up waitFor based code. waitFor allows us to deal with data dependencies. It tells the dispatcher that it should wait before going on. Here’s an example of how this approach would work out (no need to change your code!):

这个现在工作是在同步模式下.如果是在后端使用异步方式下将会出现问题.在本例中,我们设置waitFor,它让我们处理数据的以来.它会告诉调试器在继续前要等待数据反回.下面的例子展示了这种方式(这只是演式,不需要改变我们的代码):

NoteActions.create({task: 'New task'});

// Triggers waitFor
LaneActions.attachToLane({laneId});

app/stores/LaneStore.js

class LaneStore {
  ...
  attachToLane({laneId, noteId}) {
    if(!noteId) {
      this.waitFor(NoteStore);

      noteId = NoteStore.getState().notes.slice(-1)[0].id;
    }

    ...
  }
}

Fortunately, we can avoid waitFor in this case. You should use it carefully. It becomes necessary when you need to deal with asynchronously fetched data that depends on each other, however.

幸运的是,我们能在本例中避免waitFor.但是在获取异步数据时是有必要的.

实现 Edit/Remove


We are still missing some basic functionality, such as editing and removing lanes. Copy Note.jsx as Editable.jsx. We’ll get back to that original Note.jsx later in this project. For now, we just want to get Editable into a good condition. Tweak the code as follows to generalize the implementation:

我们仍没有一些基本函数.如lane的编辑和删除.复制Note.jsx改名为Editable.jsx.按如下方式修改代码:

app/components/Editable.jsx

import React from 'react';

leanpub-start-delete
export default class Note extends React.Component {
leanpub-end-delete
leanpub-start-insert
export default class Editable extends React.Component {
leanpub-end-insert
leanpub-start-delete
  constructor(props) {
    super(props);

    // Track `editing` state.
    this.state = {
      editing: false
    };
  }
  render() {
    // Render the component differently based on state.
    if(this.state.editing) {
      return this.renderEdit();
    }

    return this.renderNote();
  }
leanpub-end-delete
leanpub-start-insert
  render() {
    const {value, onEdit, onValueClick, editing, ...props} = this.props;

    return (
      <div {...props}>
        {editing ? this.renderEdit() : this.renderValue()}
      </div>
    );
  }
leanpub-end-insert
  renderEdit = () => {
    return <input type="text"
      ref={
leanpub-start-delete
        (e) => e ? e.selectionStart = this.props.task.length : null
leanpub-end-delete
leanpub-start-insert
        (e) => e ? e.selectionStart = this.props.value.length : null
leanpub-end-insert
      }
      autoFocus={true}
leanpub-start-delete
      defaultValue={this.props.task}
leanpub-end-delete
leanpub-start-insert
      defaultValue={this.props.value}
leanpub-end-insert
      onBlur={this.finishEdit}
      onKeyPress={this.checkEnter} />;
  };
leanpub-start-delete
  renderNote = () => {
    const onDelete = this.props.onDelete;

    return (
      <div onClick={this.edit}>
        <span className="task">{this.props.task}</span>
        {onDelete ? this.renderDelete() : null }
      </div>
    );
  };
leanpub-end-delete
leanpub-start-insert
  renderValue = () => {
    const onDelete = this.props.onDelete;

    return (
      <div onClick={this.props.onValueClick}>
        <span className="value">{this.props.value}</span>
        {onDelete ? this.renderDelete() : null }
      </div>
    );
  };
leanpub-end-insert
  renderDelete = () => {
    return <button
leanpub-start-delete
      className="delete-note"
leanpub-end-delete
leanpub-start-insert
      className="delete"
leanpub-end-insert
      onClick={this.props.onDelete}>x</button>;
  };
leanpub-start-insert
leanpub-start-delete
  edit = () => {
    // Enter edit mode.
    this.setState({
      editing: true
    });
  };
leanpub-end-delete
leanpub-end-insert
  checkEnter = (e) => {
    if(e.key === 'Enter') {
      this.finishEdit(e);
    }
  };
  finishEdit = (e) => {
    const value = e.target.value;

    if(this.props.onEdit) {
      this.props.onEdit(value);

leanpub-start-delete
      // Exit edit mode.
      this.setState({
        editing: false
      });
leanpub-end-delete
    }
  };
}

有一些重要的改变:

  • {editing ? this.renderEdit() : this.renderValue()} - This ternary selects what to render based on the editing state. Previously we had Note. Now we are using the term Value as that’s more generic.
  • {editing ? this.renderEdit() : this.renderValue()} - 这个三目操作符基于编辑的状态进行渲染.之间我们使用Note的状态.现在我们使用一个更通用的Value.
  • renderValue - Formerly this was known as renderNote(). Again, an abstraction step. Note that we refer to this.props.value and not this.props.task.
  • renderValue - 之前称为renderNote().注意,我们使用的是this.props.value而不是this.props.task.
  • renderDelete - Instead of using delete-note class, it uses more generic delete now.
  • renderDelete - 现在使用更通用的delete属性.

Because the class name changed, main.css needs small tweaks:
因为className的改变,所以main.css需要调整一下:

app/main.css

...

leanpub-start-delete
.note .task {
leanpub-end-delete
leanpub-start-insert
.note .value {
leanpub-end-insert
  /* force to use inline-block so that it gets minimum height */
  display: inline-block;
}

leanpub-start-delete
.note .delete-note {
leanpub-end-delete
leanpub-start-insert
.note .delete {
leanpub-end-insert
  ...
}
leanpub-start-delete
.note:hover .delete-note {
leanpub-end-delete
leanpub-start-insert
.note:hover .delete {
leanpub-end-insert
  visibility: visible;
}

Notes 使用 Editable

Next, we need to make Notes.jsx point at the new component. We’ll need to alter the import and the component name at render():

下一步,我们要在Notes.jsx中使用这个新组件.我们要更改这个引入和在render()中组件的名字:

app/components/Notes.jsx

import React from 'react';
import Editable from './Editable.jsx';

export default ({notes, onValueClick, onEdit, onDelete}) => {
  return (
    <ul className="notes">{notes.map(note =>
      <li className="note" key={note.id}>
        <Editable
          editing={note.editing}
          value={note.task}
          onValueClick={onValueClick.bind(null, note.id)}
          onEdit={onEdit.bind(null, note.id)}
          onDelete={onDelete.bind(null, note.id)} />
      </li>
    )}</ul>
  );
}

If you refresh the browser, you should see Uncaught TypeError: Cannot read property ‘bind’ of undefined. This has to do with that onValueClick definition we added. We will address this next.

如果你刷新浏览器,你会发现一个Uncaught TypeError: Cannot read property 'bind' of undefined错误.因为我们还没有onValueClick定义.我们将在下面处理.

连接 LaneEditable

Next, we can use this generic component to allow a Lane’s name to be modified. This will give a hook for our logic. We’ll need to alter

{name}
as follows:

下一步,我们将使用这个通用组件使Lane的名字可以被修改.对于我们的逻辑将会提供一个钩子.我们需要更改<div className='lane-name'>{name}</div>像下面这样:

app/components/Lane.jsx

...
leanpub-start-insert
import Editable from './Editable.jsx';
leanpub-end-insert

export default class Lane extends React.Component {
  render() {
    const {lane, ...props} = this.props;

    return (
      <div {...props}>
leanpub-start-delete
        <div className="lane-header">
leanpub-end-delete
leanpub-start-insert
        <div className="lane-header" onClick={this.activateLaneEdit}>
leanpub-end-insert
          <div className="lane-add-note">
            <button onClick={this.addNote}>+</button>
          </div>
leanpub-start-delete
          <div className="lane-name">{lane.name}</div>
leanpub-end-delete
leanpub-start-insert
          <Editable className="lane-name" editing={lane.editing}
            value={lane.name} onEdit={this.editName} />
          <div className="lane-delete">
            <button onClick={this.deleteLane}>x</button>
          </div>
leanpub-end-insert
        </div>
        <AltContainer
          stores={[NoteStore]}
          inject={{
            notes: () => NoteStore.getNotesByIds(lane.notes)
          }}
        >
leanpub-start-delete
          <Notes onEdit={this.editNote} onDelete={this.deleteNote} />
leanpub-end-delete
leanpub-start-insert
          <Notes
            onValueClick={this.activateNoteEdit}
            onEdit={this.editNote}
            onDelete={this.deleteNote} />
leanpub-end-insert
        </AltContainer>
      </div>
    )
  }
  editNote(id, task) {
    // Don't modify if trying set an empty value
    if(!task.trim()) {
      return;
    }

    NoteActions.update({id, task});
  }
  addNote = (e) => {
leanpub-start-insert
    // If note is added, avoid opening lane name edit by stopping
    // event bubbling in this case.
    e.stopPropagation();
leanpub-end-insert

    const laneId = this.props.lane.id;
    const note = NoteActions.create({task: 'New task'});

    LaneActions.attachToLane({
      noteId: note.id,
      laneId
    });
  };
  ...
leanpub-start-insert
  editName = (name) => {
    const laneId = this.props.lane.id;

    console.log(`edit lane ${laneId} name using ${name}`);
  };
  deleteLane = () => {
    const laneId = this.props.lane.id;

    console.log(`delete lane ${laneId}`);
  };
  activateLaneEdit = () => {
    const laneId = this.props.lane.id;

    console.log(`activate lane ${laneId} edit`);
  };
  activateNoteEdit(id) {
    console.log(`activate note ${id} edit`);
  }
leanpub-end-insert
}

If you try to edit a lane name now, you should see a log message at the console:
如果你修改一个lane的名字,你应该会在控制台看到日志:
这里写图片描述

定义 Editable 逻辑

We will need to define some logic to make this work. To follow the same idea as with Note, we can model the remaining CRUD actions here. We’ll need to set up update and delete actions in particular.

我们需要定义一些方法使其工作.遵循与Note相同的想法,我们能在这里建模其余的CRUD行为.

app/actions/LaneActions.js

import alt from '../libs/alt';

export default alt.generateActions(
  'create', 'update', 'delete',
  'attachToLane', 'detachFromLane'
);

We are also going to need LaneStore level implementations for these. They can be modeled based on what we have seen in NoteStore earlier:

我们同样需要到LaneStore中实现这些行为.可以仿照之前NoteStore的做法:

app/stores/LaneStore.js

...

class LaneStore {
  ...
  create(lane) {
    ...
  }
leanpub-start-insert
  update(updatedLane) {
    const lanes = this.lanes.map(lane => {
      if(lane.id === updatedLane.id) {
        return Object.assign({}, lane, updatedLane);
      }

      return lane;
    });

    this.setState({lanes});
  }
  delete(id) {
    this.setState({
      lanes: this.lanes.filter(lane => lane.id !== id)
    });
  }
leanpub-end-insert
  attachToLane({laneId, noteId}) {
    ...
  }
  ...
}

export default alt.createStore(LaneStore, 'LaneStore');

Now that we have resolved actions and store, we need to adjust our component to take these changes into account:

现在我们已经实现了actions和store,我们需要调整我们的组件被这些变化考虑进去:

app/components/Lane.jsx

...
export default class Lane extends React.Component {
  ...
leanpub-start-delete
  editNote(id, task) {
    // Don't modify if trying set an empty value
    if(!task.trim()) {
      return;
    }

    NoteActions.update({id, task});
  }
leanpub-end-delete
leanpub-start-insert
  editNote(id, task) {
    // Don't modify if trying set an empty value
    if(!task.trim()) {
      NoteActions.update({id, editing: false});

      return;
    }

    NoteActions.update({id, task, editing: false});
  }
leanpub-end-insert
  ...
leanpub-start-delete
  editName = (name) => {
    const laneId = this.props.lane.id;

    console.log(`edit lane ${laneId} name using ${name}`);
  };
  deleteLane = () => {
    const laneId = this.props.lane.id;

    console.log(`delete lane ${laneId}`);
  };
  activateLaneEdit = () => {
    const laneId = this.props.lane.id;

    console.log(`activate lane ${laneId} edit`);
  };
  activateNoteEdit(id) {
    console.log(`activate note ${id} edit`);
  }
leanpub-end-delete
leanpub-start-insert
  editName = (name) => {
    const laneId = this.props.lane.id;

    // Don't modify if trying set an empty value
    if(!name.trim()) {
      LaneActions.update({id: laneId, editing: false});

      return;
    }

    LaneActions.update({id: laneId, name, editing: false});
  };
  deleteLane = () => {
    const laneId = this.props.lane.id;

    LaneActions.delete(laneId);
  };
  activateLaneEdit = () => {
    const laneId = this.props.lane.id;

    LaneActions.update({id: laneId, editing: true});
  };
  activateNoteEdit(id) {
    NoteActions.update({id, editing: true});
  }
leanpub-end-insert
}

Try modifying a lane name now. Modifications now should get saved the same way as they do for notes. Deleting lanes should be possible as well.

尝试修改和删除方法都是好使的.
这里写图片描述

Kanban Board增加样式


As we added Lanes to the application, the styling went a bit off. Add the following styling to make it a little nicer:

我们把我们的应用变的漂亮一些:

app/main.css

body {
  background: cornsilk;
  font-family: sans-serif;
}

leanpub-start-insert
.lane {
  display: inline-block;

  margin: 1em;

  background-color: #efefef;
  border: 1px solid #ccc;
  border-radius: 0.5em;

  min-width: 10em;
  vertical-align: top;
}

.lane-header {
  overflow: auto;

  padding: 1em;

  color: #efefef;
  background-color: #333;

  border-top-left-radius: 0.5em;
  border-top-right-radius: 0.5em;
}

.lane-name {
  float: left;
}

.lane-add-note {
  float: left;

  margin-right: 0.5em;
}

.lane-delete {
  float: right;

  margin-left: 0.5em;

  visibility: hidden;
}
.lane-header:hover .lane-delete {
  visibility: visible;
}

.add-lane, .lane-add-note button {
  cursor: pointer;

  background-color: #fdfdfd;
  border: 1px solid #ccc;
}

.lane-delete button {
  padding: 0;

  cursor: pointer;

  color: white;
  background-color: rgba(0, 0, 0, 0);
  border: 0;
}
leanpub-end-insert

...

这里写图片描述

As this is a small project, we can leave the CSS in a single file like this. In case it starts growing, consider separating it to multiple files. One way to do this is to extract CSS per component and then refer to it there (e.g., require(‘./lane.css’) at Lane.jsx).

这只是一个小工程,我们可以把所有CSS放到一个文件中,如果持续的发展,考虑分离它的多个文件,一种方式是提取每个组件的CSS分别引用,例如(require('./lane.css')Lane.jsx);

Besides keeping things nice and tidy, Webpack’s lazy loading machinery can pick this up. As a result, the initial CSS your user has to load will be smaller. I go into further detail later as I discuss styling at Styling React.

除了保持整洁,可以采用Webpack的懒加载机制,最终,初始时加载的CSS会非常小,我以后会在Styling React中进一步的讨论.

组件与命名空间

So far, we’ve been defining a component per file. That’s not the only way. It may be handy to treat a file as a namespace and expose multiple components from it. React provides namespaces components just for this purpose. In this case, we could apply namespacing to the concept of Lane or Note. This would add some flexibility to our system while keeping it simple to manage. By using namespacing, we could end up with something like this:

到止前为止,我们在每个文件中定义组件,但不只这一种方式,使用命名空间可以方便的在一个文件中导出多个组件,在例子中,我们应用命名空间的概念在Note和Lane.这会方便我们的管理:

app/components/Lanes.jsx

import React from 'react';
import Lane from './Lane.jsx';

export default ({lanes}) => {
  return (
    <div className="lanes">{lanes.map(lane =>
leanpub-start-delete
      <Lane className="lane" key={lane.id} lane={lane} />
leanpub-end-delete
leanpub-start-insert
      <Lane className="lane" key={lane.id} lane={lane}>
        <Lane.Header name={lane.name} />
        <Lane.Notes notes={lane.notes} />
      </Lane>
leanpub-start-insert
    )}</div>
  );
}

app/components/Lane.jsx

...

class Lane extends React.Component {
  ...
}

Lane.Header = class LaneHeader extends React.Component {
  ...
}
Lane.Notes = class LaneNotes extends React.Component {
  ...
}

export default Lane;

Now we have pushed the control over Lane formatting to a higher level. In this case, the change isn’t worth it, but it can make sense in a more complex case.

现在我们提升了Lane的级别.在本例中这个更变是不值得的.但在更复杂的系统中会有用处.

总结


The current design has been optimized with drag and drop operations in mind. Moving notes within a lane is a matter of swapping ids. Moving notes from one lane to another is again an operation over ids. This structure leads to some complexity as we need to track ids, but it will pay off in the next chapter.

There isn’t always a clear cut way to model data and relations. In other scenarios, we could push the references elsewhere. For instance, the note to lane relation could be inversed and pushed to Note level. We would still need to track their order within a lane somehow. We would be pushing the complexity elsewhere by doing this.

Currently, NoteStore is treated as a singleton. Another way to deal with it would be to create NoteStore per Notes dynamically. Even though this simplifies dealing with the relations somewhat, this is a Flux anti-pattern better avoided. It brings complications of its own as you need to deal with store lifecycle at the component level. Also dealing with drag and drop logic will become hard.

We still cannot move notes between lanes or within a lane. We will solve that in the next chapter, as we implement drag and drop.

下一章将学习拖拽.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值