+ );
+ }
+}
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-1/loc/en-us.js b/samples/todo-webpart-sample/src/webparts/todo-step-1/loc/en-us.js
new file mode 100644
index 000000000..89f98bc1e
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-1/loc/en-us.js
@@ -0,0 +1,7 @@
+define([], function() {
+ return {
+ "PropertyPaneDescription": "Description",
+ "BasicGroupName": "Group Name",
+ "DescriptionFieldLabel": "Description Field"
+ }
+});
\ No newline at end of file
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-1/loc/mystrings.d.ts b/samples/todo-webpart-sample/src/webparts/todo-step-1/loc/mystrings.d.ts
new file mode 100644
index 000000000..5b94b8d6f
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-1/loc/mystrings.d.ts
@@ -0,0 +1,10 @@
+declare interface ITodoStrings {
+ PropertyPaneDescription: string;
+ BasicGroupName: string;
+ DescriptionFieldLabel: string;
+}
+
+declare module 'todoStrings' {
+ const strings: ITodoStrings;
+ export = strings;
+}
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-1/tests/Todo.test.ts b/samples/todo-webpart-sample/src/webparts/todo-step-1/tests/Todo.test.ts
new file mode 100644
index 000000000..c5b8423b3
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-1/tests/Todo.test.ts
@@ -0,0 +1,7 @@
+import * as assert from 'assert';
+
+describe('TodoWebPart', () => {
+ it('should do something', () => {
+ assert.ok(true);
+ });
+});
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-2/ITodoWebPartProps.ts b/samples/todo-webpart-sample/src/webparts/todo-step-2/ITodoWebPartProps.ts
new file mode 100644
index 000000000..d5336c939
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-2/ITodoWebPartProps.ts
@@ -0,0 +1,166 @@
+/**
+ * @Copyright (c) Microsoft Corporation. All rights reserved.
+ *
+ * @file ITodoWebPartProps.tsx
+ */
+
+/**
+ * Interface for the Todo web part properties.
+ */
+export interface ITodoWebPartProps {
+ /**
+ * The current selected SharePoint tasks list.
+ */
+ selectedList: ITodoTaskList;
+
+ /**
+ * Whether to show completed tasks.
+ *
+ * If it is set to false, the tab of completed tasks will be hiden, and
+ * there will be no completed tasks shown in the list.
+ */
+ shouldShowCompletedTasks: boolean;
+
+ /**
+ * Whether to show who created the task.
+ */
+ shouldShowCreatedBy: boolean;
+
+ /**
+ * Whether to show who mark the task as complete.
+ */
+ shouldShowCompletedBy: boolean;
+
+ /**
+ * The max number of tasks showing in todo web part.
+ *
+ * The number of list items shown in the list will not exceed this number, and it also limits
+ * the number of return items when it sends request to the sever.
+ */
+ maxNumberOfTasks: number;
+}
+
+/**
+ * Interface for the data used to render the todo component.
+ */
+export interface ITodoComponentData {
+ /**
+ * The selected list items rendered in Todo Component.
+ */
+ selectedListItems?: ITodoTask[];
+
+ /**
+ * The loading status of the list items.
+ */
+ loadingStatus?: LoadingStatus;
+}
+
+/**
+ * Interface of user data model related to this web part.
+ */
+export interface ITodoPerson {
+ /**
+ * The ID of the person which used to identify the user and fetch the user data from server.
+ */
+ Id: number;
+
+ /**
+ * The name of this person.
+ */
+ Title: string;
+
+ /**
+ * The url which representing the avator url of this person.
+ */
+ Picture: string;
+
+ /**
+ * The email address of this person.
+ *
+ * This field is only used in data provider to construct url for fetch user avator.
+ */
+ EMail: string;
+}
+
+/**
+ * The interface of data modal for Todo task.
+ */
+export interface ITodoTask {
+ /**
+ * The ID of the todo item.
+ */
+ Id: number;
+
+ /**
+ * The title of the todo item.
+ */
+ Title: string;
+
+ /**
+ * The percent of the task that is completed.
+ * In todo web part we use 0 to indicate task uncompleted and 1 to indicate task completed.
+ */
+ PercentComplete: number;
+
+ /**
+ * The person who created this todo task.
+ */
+ Author: ITodoPerson;
+
+ /**
+ * The person who marked this todo item as completed.
+ *
+ * Editor is the last person who performed editting operation.
+ * In Todo web part, the only editting operation that can be performed by user is toggling complete.
+ * This field is optional because it is not required for incomplete tasks.
+ */
+ Editor?: ITodoPerson;
+}
+
+/**
+ * ITodoTaskList contains title and entity type full name which used in REST calls.
+ */
+export interface ITodoTaskList {
+ /**
+ * The title of the todo task list.
+ */
+ Title: string;
+
+ /**
+ * The ListItemEntityTypeFullName property of the list.
+ */
+ ListItemEntityTypeFullName?: string;
+
+ /**
+ * The Id property of the list.
+ */
+ Id?: string;
+}
+
+/**
+ * The type of the loading status for requesting for the items from SharePoint List.
+ */
+export enum LoadingStatus {
+ /**
+ * We are not loading anything.
+ */
+ None,
+
+ /**
+ * We are fetching the tasks items (Read items).
+ */
+ FetchingTasks,
+
+ /**
+ * We are updating the tasks list (Create, Update, or Delete the item).
+ */
+ UpdatingTasks
+}
+
+/**
+ * ItemOperationCallback is the type of callback that handle the operation on item.
+ * It is used for creating, updating and deleting callbacks.
+ *
+ * @param {ITodoTask} item is the Todo task item that will be either created, updated, or deleted.
+ */
+export type ItemOperationCallback = (item: ITodoTask) => void;
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-2/README.md b/samples/todo-webpart-sample/src/webparts/todo-step-2/README.md
new file mode 100644
index 000000000..deedec70e
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-2/README.md
@@ -0,0 +1,11 @@
+## Todo sample web part step 2
+
+In this step, we add the React components and a mock data provider for todo web part.
+
+## How to build
+
+```bash
+npm install
+gulp serve
+gulp package-solution
+```
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-2/TodoWebPart.manifest.json b/samples/todo-webpart-sample/src/webparts/todo-step-2/TodoWebPart.manifest.json
new file mode 100644
index 000000000..981ab82ad
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-2/TodoWebPart.manifest.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
+
+ "componentType": "WebPart",
+ "version": "1.0.0",
+ "manifestVersion": 2,
+ "id": "89166469-d248-498f-8400-6d124b016f13",
+ "preconfiguredEntries": [
+ {
+ "groupId": "89166469-d248-498f-8400-6d124b016f13",
+ "group": {
+ "default": "Under Development"
+ },
+ "title": {
+ "default": "Todo step 2"
+ },
+ "description": {
+ "default": "Todo sample webpart step 2"
+ },
+ "officeFabricIconFontName": "BulletedList",
+ "properties": {
+ "selectedList": {
+ "Title": "DefaultTodoList"
+ },
+ "shouldShowCompletedTasks": true,
+ "shouldShowCompletedBy": true,
+ "shouldShowCreatedBy": true,
+ "maxNumberOfTasks": 10
+ }
+ }
+ ]
+}
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-2/TodoWebPart.tsx b/samples/todo-webpart-sample/src/webparts/todo-step-2/TodoWebPart.tsx
new file mode 100644
index 000000000..3754feb86
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-2/TodoWebPart.tsx
@@ -0,0 +1,250 @@
+/**
+ * @Copyright (c) Microsoft Corporation. All rights reserved.
+ *
+ * @file TodoWebPart.tsx
+ */
+
+import * as lodash from '@microsoft/sp-lodash-subset';
+import update = require('react-addons-update');
+
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+
+import {
+ BaseClientSideWebPart,
+ IWebPartContext
+} from '@microsoft/sp-client-preview';
+
+import { ITodoDataProvider } from './dataProviders/ITodoDataProvider';
+import MockTodoDataProvider from './dataProviders/MockTodoDataProvider';
+
+import {
+ ITodoWebPartProps,
+ ITodoComponentData,
+ ITodoTaskList,
+ ITodoTask,
+ LoadingStatus
+} from './ITodoWebPartProps';
+
+import Todo, { ITodoProps } from './components/Todo';
+
+/**
+ * This is the client-side todo sample web part built using the SharePoint Framework.
+ * It could interact with tasks lists in SharePoint site.
+ *
+ * Find out more docs and tutorials at:
+ * https://github.com/SharePoint/sp-dev-docs/wiki
+ */
+export default class TodoWebPart extends BaseClientSideWebPart {
+ private _dataProvider: ITodoDataProvider;
+
+ private _shouldGetLists: boolean;
+ private _listTitleToList: { [title: string]: ITodoTaskList };
+
+ private _todoComponentData: ITodoComponentData;
+
+ constructor(context: IWebPartContext) {
+ super(context);
+
+ this._shouldGetLists = true;
+
+ this._todoComponentData = {
+ selectedListItems: [],
+ loadingStatus: LoadingStatus.None
+ };
+
+ this._renderTodoComponent = this._renderTodoComponent.bind(this);
+ this._readLists = this._readLists.bind(this);
+ this._ensureSelectedList = this._ensureSelectedList.bind(this);
+ this._createItem = this._createItem.bind(this);
+ this._readItems = this._readItems.bind(this);
+ this._updateItem = this._updateItem.bind(this);
+ this._deleteItem = this._deleteItem.bind(this);
+ this._toggleComplete = this._toggleComplete.bind(this);
+ }
+
+ /**
+ * Override the base onInit() implementation to get the persisted properties to initialize data provider.
+ */
+ public onInit(): Promise {
+ this._dataProvider = new MockTodoDataProvider(this.properties);
+
+ return Promise.resolve(undefined);
+ }
+
+ /**
+ * Override the base render() implementation to render the todo sample web part.
+ */
+ public render(): void {
+ this._renderTodoComponent();
+
+ this._ensureSelectedList();
+ }
+
+ protected dispose(): void {
+ ReactDOM.unmountComponentAtNode(this.domElement);
+
+ super.dispose();
+ }
+
+ private _renderTodoComponent(partialData?: ITodoComponentData): void {
+ lodash.extend(this._todoComponentData, partialData);
+
+ ReactDOM.render(
+ as React.ReactElement,
+ this.domElement
+ );
+ }
+
+ /**
+ * Read the information of all the task lists stored in the current site through data provider.
+ */
+ private _readLists(): Promise {
+ return this._dataProvider.readLists()
+ .then((lists: ITodoTaskList[]) => {
+ // Create map from list title to the list
+ this._listTitleToList = {};
+ lists.forEach((list: ITodoTaskList) => {
+ this._listTitleToList[list.Title] = list;
+ });
+ });
+ }
+
+ /**
+ * If there is no GUID for the selected task list in properties, we will read all task lists on the
+ * site and retrieve the GUID. Usually it will happen at the first time we add todo web part, since
+ * in manifest we could not predict the GUID for the default list added by SharePoint feature XML.
+ * Finally we will read task items of the selected list.
+ */
+ private _ensureSelectedList(): Promise {
+ if (!this.properties.selectedList.Id) {
+ this.clearError();
+ this._renderTodoComponent({ loadingStatus: LoadingStatus.FetchingTasks });
+
+ return this._dataProvider.readLists()
+ .then((lists: ITodoTaskList[]) => {
+ const selectedLists: ITodoTaskList[] = lists.filter((list: ITodoTaskList) => {
+ return list.Title === this.properties.selectedList.Title;
+ });
+
+ this.properties.selectedList = selectedLists[0] || lists[0];
+ this._dataProvider.selectedList = this.properties.selectedList;
+ })
+ .then(this._readItems, (error: Error) => {
+ this._renderTodoComponent({ loadingStatus: LoadingStatus.None });
+ this.renderError(error);
+ });
+ } else if (!this.renderedOnce) {
+ return this._readItems();
+ } else {
+ // The list id exists and items have been fetched, do nothing.
+ return Promise.resolve(undefined);
+ }
+ }
+
+ /**
+ * Create a new item and add it to the list through data provider.
+ */
+ private _createItem(item: ITodoTask): Promise {
+ this.clearError();
+ this._renderTodoComponent({ loadingStatus: LoadingStatus.UpdatingTasks });
+
+ return this._dataProvider.createItem(item.Title)
+ .then(
+ (items: ITodoTask[]) => items && this._renderTodoComponent({
+ selectedListItems: items,
+ loadingStatus: LoadingStatus.None
+ }),
+ (error: Error) => {
+ this._renderTodoComponent({ loadingStatus: LoadingStatus.None });
+ this.renderError(error);
+ }
+ );
+ }
+
+ /**
+ * Read the list items from the data provider.
+ */
+ private _readItems(): Promise {
+ this.clearError();
+
+ if (this._dataProvider.maxNumberOfTasks > this._todoComponentData.selectedListItems.length) {
+ this._renderTodoComponent({ loadingStatus: LoadingStatus.FetchingTasks });
+
+ return this._dataProvider.readItems()
+ .then(
+ (items: ITodoTask[]) => items && this._renderTodoComponent({
+ selectedListItems: items,
+ loadingStatus: LoadingStatus.None
+ }),
+ (error: Error) => {
+ this._renderTodoComponent({ loadingStatus: LoadingStatus.None });
+ this.renderError(error);
+ }
+ );
+ } else {
+ this._renderTodoComponent({
+ selectedListItems: this._todoComponentData.selectedListItems.slice(0, this._dataProvider.maxNumberOfTasks)
+ });
+
+ return Promise.resolve(undefined);
+ }
+ }
+
+ /**
+ * Update a item in the list through data provider.
+ */
+ private _updateItem(newItem: ITodoTask): Promise {
+ this.clearError();
+
+ const updatingIndex: number = lodash.findIndex(this._todoComponentData.selectedListItems,
+ (item: ITodoTask) => item.Id === newItem.Id
+ );
+ this._renderTodoComponent({
+ selectedListItems: update(this._todoComponentData.selectedListItems, { [updatingIndex]: { $set: newItem } })
+ });
+
+ return this._dataProvider.updateItem(newItem)
+ .then(
+ (items: ITodoTask[]) => items && this._renderTodoComponent({ selectedListItems: items }),
+ this.renderError
+ );
+ }
+
+ /**
+ * Delete a item from the list through data provider.
+ */
+ private _deleteItem(item: ITodoTask): Promise {
+ this.clearError();
+
+ this._renderTodoComponent({
+ selectedListItems: this._todoComponentData.selectedListItems.filter((task: ITodoTask) => task.Id !== item.Id)
+ });
+
+ return this._dataProvider.deleteItem(item)
+ .then(
+ (items: ITodoTask[]) => items && this._renderTodoComponent({ selectedListItems: items }),
+ this.renderError
+ );
+ }
+
+ /**
+ * Toggle the complete state of an item by.
+ *
+ * Will call updateItem function to update complete state of this item.
+ */
+ private _toggleComplete(item: ITodoTask): Promise {
+ // Create a new Item in which the PercentComplete value has been changed.
+ const newItem: ITodoTask = update(item, {
+ PercentComplete: { $set: item.PercentComplete >= 1 ? 0 : 1 }
+ });
+
+ return this._updateItem(newItem);
+ }
+}
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-2/common/Utils.ts b/samples/todo-webpart-sample/src/webparts/todo-step-2/common/Utils.ts
new file mode 100644
index 000000000..e81f1590f
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-2/common/Utils.ts
@@ -0,0 +1,37 @@
+/**
+ * @Copyright (c) Microsoft Corporation. All rights reserved.
+ *
+ * @file Utils.ts
+ */
+
+// Regex that finds { and } so they can be removed on a lookup for string format
+const FORMAT_ARGS_REGEX: RegExp = /[\{\}]/g;
+
+// Regex that finds {#} so it can be replaced by the arguments in string format
+const FORMAT_REGEX: RegExp = /\{\d+\}/g;
+
+/**
+ * String Format is like C# string format.
+ * Usage Example: "hello {0}!".format("mike") will return "hello mike!"
+ * Calling format on a string with less arguments than specified in the format is invalid
+ * Example "I love {0} every {1}".format("CXP") will result in a Debug Exception.
+ */
+/* tslint:disable:no-any no-null-keyword export-name */
+export function format(s: string, ...values: any[]): string {
+ 'use strict';
+
+ const args: any[] = values;
+ // Callback match function
+ function replace_func(match: string): any {
+ // looks up in the args
+ let replacement: any = args[match.replace(FORMAT_ARGS_REGEX, '')];
+
+ // catches undefined in nondebug and null in debug and nondebug
+ if (replacement === null) {
+ replacement = '';
+ }
+ return replacement;
+ }
+ return (s.replace(FORMAT_REGEX, replace_func));
+}
+/* tslint:enable:no-any no-null-keyword export-name */
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-2/components/Todo.tsx b/samples/todo-webpart-sample/src/webparts/todo-step-2/components/Todo.tsx
new file mode 100644
index 000000000..701d6ae23
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-2/components/Todo.tsx
@@ -0,0 +1,102 @@
+/**
+ * @Copyright (c) Microsoft Corporation. All rights reserved.
+ *
+ * @file Todo.tsx
+ */
+
+import * as React from 'react';
+import { Compare } from '@microsoft/sp-client-base';
+import {
+ Spinner,
+ SpinnerType
+} from 'office-ui-fabric-react';
+
+import {
+ ITodoWebPartProps,
+ ITodoComponentData,
+ LoadingStatus,
+ ItemOperationCallback
+} from '../ITodoWebPartProps';
+
+import TodoForm from './TodoForm';
+import TodoTabs from './TodoTabs';
+
+import * as strings from 'todoStrings';
+import styles from '../style/Todo.module.scss';
+
+/**
+ * Props for Todo component.
+ */
+export interface ITodoProps extends ITodoComponentData, ITodoWebPartProps {
+ /**
+ * onCreateItem callback triggered when we add a new item to the tasks list.
+ * Either triggered by clicking on add button or pressed Enter key in input field.
+ */
+ onCreateItem: ItemOperationCallback;
+
+ /**
+ * onToggleComplete callback triggered when checkbox of one item is checked or unchecekd.
+ */
+ onToggleComplete: ItemOperationCallback;
+
+ /**
+ * onDeleteItem callback triggered when delete of one item is triggered.
+ */
+ onDeleteItem: ItemOperationCallback;
+}
+
+/**
+ * Todo component is the top level react component of this web part.
+ * It uses fabric-react component
+ *
+ * Link of Spinner: https://fabricreact.azurewebsites.net/fabric-react/master/#/examples/spinner
+ */
+export default class Todo extends React.Component {
+ public shouldComponentUpdate(nextProps: ITodoProps, nextState: {}): boolean {
+ return !Compare.shallowCompare(this.props, nextProps) || !Compare.shallowCompare(this.state, nextState);
+ }
+
+ public render(): React.ReactElement> {
+ return (
+
+ )
+ : null; // tslint:disable-line:no-null-keyword
+ }
+}
diff --git a/samples/todo-webpart-sample/src/webparts/todo-step-2/components/TodoForm.tsx b/samples/todo-webpart-sample/src/webparts/todo-step-2/components/TodoForm.tsx
new file mode 100644
index 000000000..8ea8167b1
--- /dev/null
+++ b/samples/todo-webpart-sample/src/webparts/todo-step-2/components/TodoForm.tsx
@@ -0,0 +1,133 @@
+/**
+ * @Copyright (c) Microsoft Corporation. All rights reserved.
+ *
+ * @file TodoForm.tsx
+ */
+
+import * as React from 'react';
+import { Compare } from '@microsoft/sp-client-base';
+import {
+ TextField,
+ Button,
+ ButtonType
+} from 'office-ui-fabric-react';
+
+import {
+ ITodoTask,
+ ItemOperationCallback
+} from '../ITodoWebPartProps';
+import * as strings from 'todoStrings';
+import styles from '../style/Todo.module.scss';
+
+/**
+ * Props for TodoForm component.
+ */
+export interface ITodoFormProps {
+ /**
+ * onSubmit callback triggered when the form is submitted.
+ * Either triggered by clicking on add button or pressed Enter key in input field.
+ */
+ onSubmit: ItemOperationCallback;
+}
+
+/**
+ * States for TodoForm component.
+ */
+export interface ITodoFormState {
+ /**
+ * inputValue is the react state of input box value.
+ */
+ inputValue: string;
+
+ /**
+ * The error message will show below the input box if the title filled in is invalid.
+ */
+ errorMessage: string;
+}
+
+/**
+ * The form component used for adding new item to the list.
+ *
+ * It uses fabric-react component