DXWorkout Demo

PhoneJS comes with the DXWorkout demo - an application that is designed to help those who wish to plan their workouts in a gym, track the history and analyze the results. While you can use this application as a real-world application, it is still rather simplified for demo purposes. In this tutorial, you will learn how this application is organized and how its features are implemented using PhoneJS.

Try the DXWorkout application in the simulator to the right. To see the application's code, find the Demos/DXWorkout folder in the PhoneJS zip archive. In addition, you can download this app from the GitHub resource.

Application Structure

The application is organized by following the application template that is supplied with PhoneJS. Different sections of the application project are detailed below so that you can find your way within the application easily when investigating it.

Application Page

The application's single page markup is designed in the index.html file. In this file, you will find a div element with the dx-viewport class applied. This element is used to embed the application views that replace each other during application flow. These views are designed separately, but all of them are linked on this page. In addition, links to the required libraries are also given here.

HTML
<script type="text/javascript" src="js/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="js/knockout-3.0.0.js"></script>
<script type="text/javascript" src="js/globalize.min.js"></script>
<!--The library that includes both the PhoneJS and ChartJS scripts-->
<script type="text/javascript" src="js/dx.all.js"></script>

Application Start

The start point of the application is the creation of the HTMLApplication object and a call of its navigate() method. Investigate the code given in the index.js file. Here is the function that is called when starting the application.

JavaScript
$(function () {
    app = wo.app = new DevExpress.framework.html.HtmlApplication(APP_SETTINGS);
    app.router.register(":view/:action/:item", { view: "Home", action: undefined, item: undefined });
    //...
    //Initializes data for the application
    wo.initUserData();
    //Calls the navigate() method to navigate to the "Home" default view
    startApp();
});

View HTML Templates and ViewModels

Like all PhoneJS applications, the DXWorkout application is based on the MVVM (Model-View-ViewModel) pattern. All the views that you see in the running DXWorkout application are defined by corresponding HTML templates (Views, in conjunction with CSS styles) and ViewModels from the views folder.

Note that a view is not necessarily defined by an individual HTML template. The "List" HTML template is used for presenting the Select Goal, Select Exercise, Edit Goals and Edit Exercises views. This is possible due to the ability to set conditions in HTML markup.

HTML
<div class="search-wrapper" data-bind="ifnot: isEdit">
      <div data-bind="dxTextBox: { placeholder: 'Search', mode: 'search', value: searchQuery, valueUpdateEvent: 'keyup', showSearchField: true } "></div>
</div>
<div class="new-item" data-bind="if: isEdit">
      <div data-bind="dxTextBox: { value: newValue, placeholder: emptyValue } "></div>
      <div data-bind="dxButton: { text: 'Add', clickAction: handleAddClick }"></div>
</div>

The business model of this application includes the Workout, Exercise and Set business objects. Since each of these objects is used in different views, shared ViewModels were implemented to manipulate the current workout, exercise and set, respectively. You can find the code of these ViewModels in the viewModels folder. The following code demonstrates how the Workout and Exercise ViewModels are embedded into the ViewModel of the "Exercise" view.

JavaScript
DXWorkout.Exercise = function(params) {
      var wo = DXWorkout,
      workout = ko.observable(),
      //...
      return
      //Exercise ViewModel object
      {
            //...
            workout: workout,
            exercise: exercise,
            //...
            viewShowing: function(args) {
                  //The current Workout ViewModel
                  var currentWorkout = wo.currentWorkout,
                  //The current Exercise ViewModel
                  currentExercise = currentWorkout.currentExercise(),
                  //...
                  workout(currentWorkout);
                  exercise(currentExercise);
                  //...
            }
      }
};

Application Style Sheets

As a PhoneJS project, DXWorkout includes the css folder with built-in styles for all supported devices. In addition, you can find custom style sheets in the index.css file. They are used in HTML view templates. For instance, buttons are made larger to make them easier to use in a gym.

CSS
.actions .dx-button {
      margin: 10px 0;
      padding: 0;
      width: 100%;
      height: 40px;
}

Navigation

To provide the Slide Out type of a global navigation in the application, the SlideOut navigation type is used.

To specify the views to be navigated from the Slide-Out menu, the navigation option is set within the HtmlApplication object's configuration object.

JavaScript
var //...
APP_SETTINGS = {
        namespace: wo,
        navigationType: "slideout",
        navigation: [
        {
                "id": "Home",
                "title": "Home",
                "action": "#Home",
                "icon": "home"
        },
        {
                "id": "Logs",
                "title": "Logs",
                "action": "#Log",
                "icon": "event"
        },
        {
                "id": "Graphs",
                "title": "Graphs",
                "action": "#GoalGraphs",
                "icon": "chart"
        },
        {
                "id": "Settings",
                "title": "Settings",
                "action": "#Settings",
                "icon": "card"
        }
        ]
};
//...
$(function () {
        app = wo.app = new DevExpress.framework.html.HtmlApplication(APP_SETTINGS);
        app.router.register(":view/:action/:item", { view: "Home", action: undefined, item: undefined });
        //...
}

Application Data

All test data used in this application is located in the js/sampleData.js file in the form of an array of objects. If you want to use this application as a real application, just remove this file and add your data while using the application. The data that you add or change, together with the predefined data (if you are using any), is saved to a storage using a data layer. In this application, a local storage is used as a data layer. The functions that are required to manipulate the local storage are collected within the userDataLocal.js file. In case another data layer is required to be used in the application, the userDataLocal.js file will simply be replaced with another one in which the same functions will work with the new storage. Read below for details on data.

Work with Data

The data that you operate with in the DXWorkout application is the following:

  • settings - lists of possible goals, exercises, length units and weight units;

  • workouts - a list of workouts with a particular goal selected and exercise(s) and set(s) accomplished;

  • current workout - the currently unfinished workout.

When you start the application for the very first time, the workouts from the sampleData array are loaded to the memory and can be accessed via the wo.workouts variable (wo is a local variable used for shortening the "DXWorkout" global variable name). The predefined settings that are specified in the defaultSettings array are also loaded to the memory and can be accessed via the wo.settings variable.

JavaScript
function initSetting(key) {
    var settingsFromStorage = localStorage.getItem("dxworkout-settings-" + key),
        currentSettings;
    if(settingsFromStorage) {
        currentSettings = JSON.parse(settingsFromStorage); 
    } else {
       currentSettings = wo.defaultSettings[key]; 
    }        
    wo.settings[key] = isObservableSetting(key) ? ko.observable(currentSettings) : currentSettings;
}

function initUserData() {
    if(localStorage.getItem(DATA_VERSION_KEY) !== DATA_VERSION) {
        clearUserData();
        localStorage.setItem(DATA_VERSION_KEY, DATA_VERSION);
    }
    initSetting("goal");
    initSetting("exercise");
    initSetting("lengthUnit");
    initSetting("weightUnit");
    var storageData = localStorage.getItem(WORKOUTS_KEY);
    var data = storageData ? JSON.parse(storageData) : wo.sampleData;
    workoutArray = wo.workouts = ko.observableArray(data);
}

When you press Start Workout in the "Home" view, a Workout ViewModel is created and becomes the current workout that can be accessed via the wo.currentWorkout variable. Then the current workout is detailed by the selected goal, added exercises and accomplished sets. All these operations are performed via the functions that are exposed by the Workout ViewModel object.

JavaScript
DXWorkout.createWorkoutViewModel = function() {
      //...
      return {
        goalList: wo.settings['goal'],
        weightUnit: DXWorkout.settings["weightUnit"],

        id: id,
        duration: duration,
        goal: goal,
        startDate: startDate,
        endDate: endDate,
        notes: notes,
        exercises: exercises,
        currentExercise: currentExercise,

        handleAddSet: handleAddSet,
        handleAddExercise: handleAddExercise,
        handleShowResults: handleShowResults,

        handleContinue: handleContinue,
        handleFinish: handleFinish,
        handleDelete: handleDelete,

        handleNotesChange: handleNotesChange,

        save: save,
        clear: clear,
        clearCurrentExercise: clearCurrentExercise,
        cancelCurrentWorkout: cancelCurrentWorkout,

        toJS: toJS,
        fromJS: fromJS
    };
};

When you press Finish Workout in the "Workout Results" view, the current workout is added to the wo.workouts array and all this array is saved to the data storage. Then, when you add notes to a workout or delete a workout (selecting the workout from the "Settings" view), the changes are saved to the storage as well. In this application, local storage is used as data storage. Here is an example of how a workout is saved to the local storage.

JavaScript
function insertWorkout(workout) {
    workoutArray.push(workout);
    saveWorkouts();
}
  function saveWorkouts() {
    localStorage.setItem(WORKOUTS_KEY, JSON.stringify(workoutArray()));
}

As you can see, workouts are stored as strings in the local storage.

The next time when you run the application, the data is loaded from the storage if there is one there.

The same technique is used for settings (lists of possible goals, exercises, etc.). For instance, when you change the list of possible goals in the "Settings" view, the resulting list is saved to the data storage and then loaded from there the next time the application runs (see the initUserData() function above).

To prevent losing data about the current workout when the application is unloaded for some reasons (e.g., an income call), this workout is saved to the local storage as the current workout.

JavaScript
function saveCurrentWorkout() {
    if(!wo.currentWorkout)
        return;
    var workout = wo.currentWorkout.toJS(),
        exercises = workout.exercises;  
    if(!workout.goal || exercises.length === 0)
        return;
    if(!exercises[exercises.length - 1].name)
        exercises.pop();
    if(exercises.length)
        localStorage.setItem(CURRENT_KEY, JSON.stringify(workout));
}

When starting the application the next time, the local storage is checked for the last unfinished workout. If in it and you want to continue it, a ViewModel is created for it and the "Workout Results" view is shown.

JavaScript
function startApp() {
    var current = wo.getCurrentFromStorage();

    if(current && confirm("Do you want to continue workout in progress?")) {
        var workout = wo.createWorkoutViewModel();
        workout.fromJS(current);
        wo.currentWorkout = workout;
        wo.app.navigate("Results");
        return;
    }

    wo.removeCurrentWorkout();
    wo.app.navigate();
}

Home View

DXWorkout Home View

This view enables the following capabilities:

  • start a new workout;

  • see the last work date;

  • see how many days have passed since the last workout;

  • investigate how long the previous workouts lasted, using a chart.

This view is created using the ViewModel returned by the Home function (see the Home.js file) and the "Home" HTML template (see the Home.html file).

To display values for the Last workout date and Days since last workout fields when the view is shown, these fields are bound to the lastWorkoutDate and daysFromLastWorkout fields of the view's ViewModel via Knockout. The ViewModel fields are calculated based on the data loaded when starting the application. To update the ViewModel's fields each time the view is shown, the calculation is performed within the viewShowing event handler, which is called when the view is close to be shown.

JavaScript
DXWorkout.Home = function(params) {
    //...
    return {
          lastWorkoutDate: lastWorkoutDate,
          daysFromLastWorkout: daysFromLastWorkout,
        //...
          viewShowing: function() {
              var workouts = DXWorkout.workouts();
              currentDate = new Date;
              if(!workouts || !workouts.length) {
                  chartDataSource([]);
                  daysFromLastWorkout(EMPTY_TEXT);
                  lastWorkoutDate(EMPTY_TEXT);
                  return;
              }
              updateChartDataSource(workouts);
              workouts.sort(function(i, j) {
                  return new Date(j.startDate) - new Date(i.startDate);
              });            
              var lastDate = new Date(workouts[0].startDate);
              lastWorkoutDate(Globalize.format(lastDate, 'MMM d, yyyy'));
              daysFromLastWorkout(new Date(currentDate - lastDate).getDate() - 1);
          }
      };
};

As you can see, data for the chart is prepared analogously. To display a chart, the dxChart widget from the ChartJS library is used.

HTML
<div data-options="dxView : { name: 'Home', title: 'Home' } " > 
      <div data-options="dxContent : { targetPlaceholder: 'content' } " >
        <!--...-->
          <div class="graph-area" data-bind="if: hasWorkouts">
              <div id="goalsChart" class="home-graph" data-bind="dxChart: chartOptions"></div>
          </div>
      </div>
</div>
JavaScript
DXWorkout.Home = function(params) {
        //...
      chartOptions = {
              series: {
                    argumentField: 'startDate',
                    valueField: 'duration',
              },
              title: {
                    visible: false,
                    font: { size: 18 }
              },
              legend: { visible: false },
              argumentAxis: {
                    label: { format: 'monthAndDay' },
                    grid: { visible: true },
                    tickInterval: 'day'
              },
              valueAxis: {
                    min: 0,
                    tickInterval: 'hours',
                    label: {
                          customizeText: function(data) {
                                return wo.formatTime(data.value);
                          }
                    }
              },
              tooltip: {
                    enabled: true,
                    customizeText: function() {
                          return wo.formatTime(this.value);
                    }
              },
              palette: 'Soft Pastel',
              dataSource: chartDataSource
        };
        return {
              //...
              chartOptions: chartOptions,
              //...
        }
}

To start a workout, the dxButton's clickAction option is bound to a function of the view's ViewModel. In this function, a Workout ViewModel is created and the application navigates you to the "List" view passing "goal" as a parameter.

JavaScript
function handleAddWorkout() {
    var workout = wo.createWorkoutViewModel();
    workout.clear();
    wo.currentWorkout = workout;
    wo.app.navigate("List/select/goal");
}

Generally, navigation is automated in PhoneJS applications. You should only specify a routing rule for navigation on an application start and then call the navigate() method where required. In the DXWorkout application, the following routing is specified:

JavaScript
app = wo.app = new DevExpress.framework.html.HtmlApplication(APP_SETTINGS);
app.router.register(":view/:action/:item", { view: "Home", action: undefined, item: undefined });

We passed "goal" as the item parameter so that the "List" view know that goals must be retrieved from the settings data to be shown in the list.

Select Goal View

DXWorkout Home View

In this view, you can select a goal for your workout. You can find the required goal by scrolling the list or by using the Search box. Otherwise, you can return to the previous view.

This view is created using the ViewModel returned by the List function (see the List.js file) and the "List" HTML template (see the List.html file).

To display a list of goals, the dxList widget is used.

HTML
<div data-bind="dxList: { items: keySettings, itemClickAction: handleItemClick }">
    <div data-options="dxTemplate : { name: 'item' }">
        <div class="list-item-title">
            <span data-bind="text: $data"></span>
        </div>
        <div data-bind="if: $root.canDelete">
            <div data-bind="dxButton: { text: 'Delete', clickAction: $root.handleDeleteClick }"></div>
        </div>
    </div>
</div>

The items option of the widget is bound to the keySettings field used to provide data for the list items. The itemClickAction option is bound to the handleItemClick function which is performed when clicking a list item.

JavaScript
function handleItemClick(e) {
    var workout = wo.currentWorkout;
    if(!isEdit) {
        switch(key) {
            case "goal":
                workout.goal(e.itemData);
                workout.handleAddExercise();
                break;
            //...
        }
    }
};

To use a single template for all list items, an item's HTML markup is wrapped by a div element with the data-options attribute set to dxTemplate.

Search Box

To search within the list for the required item, a text box is added using the dxTextBox widget.

HTML
<div class="search-wrapper" data-bind="ifnot: isEdit">
    <div data-bind="dxTextBox: { placeholder: 'Search', mode: 'search', value: searchQuery, valueUpdateEvent: 'keyup', showSearchField: true } "></div>
</div>

When the input value is changed, the value of the keySettings field, to which the widget's items option is bound, is updated. As a result, the list is updated with the new data.

JavaScript
searchQuery.subscribe(function(value) {
    var result = $.grep(wo.settings[key], function(product, index) {
        var regExp = new RegExp(value, "i");
        return !!product.match(regExp);
    });
    keySettings(result);
});

Back Button

When implementing the Back button, we considered that there is a hardware Back button on Android, Tizen and Win8Phone devices. But for iOS devices, we implemented a dxCommand that is displayed as a "back" button and performs a specified action.

HTML
<div data-bind="dxCommand: { title: 'Back', location: 'back', type: 'back', id: 'back', action: handleCancel, visible: !DXWorkout.hardwareBackButton }"></div>

However, this command is executed in Android, Tizen and Win8Phone devices anyway, when pressing the hardware Back button. For this purpose, the currentBackAction function is called, when the hardware Back button is pressed. This function is associated with the ViewModel's function that must be executed at this time.

JavaScript
function onDeviceReady() {
    document.addEventListener("backbutton", onBackButton, false);
    //...
}
function onBackButton() {
    if(currentBackAction) {
        currentBackAction();
    } else {
        //...
    }
}
  function onViewShown(args) {
    var viewInfo = args.viewInfo;
    //...
    currentBackAction = viewInfo.model.backButtonDown;
}

In the "List" ViewModel, the handleCancel function is assigned to be called when clicking the hardware Back Button.

  <!--JavaScript-->DXWorkout.List = function(params) {
      //...
      return {
              //The field to which the view's Back button is bound
              handleCancel: handleCancel,
              //The field to which the hardware Back button is bound
          backButtonDown: handleCancel
              //...
      };
  }

Select Exercise View

DXWorkout Home View

In this view, you can select one of the exercises that are associated with the goal that you selected on the previous view. You can find the required exercise by scrolling the list or by using the Search box. Otherwise, you can return to the previous view.

The "Select Exercise" view is created using the same ViewModel object and HTML template as the previous view. This is possible because the items option of the dxList widget on this view is bound to the ViewModel's field that is specified when the view is showing. To determine which settings to retrieve for the list, the item navigation parameter is used.

  <!--JavaScript-->DXWorkout.List = function (params) {
      var action = params.action,
          key = params.item,
          //...
      return {
              //...
          viewShowing: function(args) {
              keySettings(wo.settings[key]);
                    //...
              }
        }   
  }

When you click an item in the Goals list, the application navigates you to the "Select Exercise" view with "exercise" passed as the item navigation parameter.

JavaScript
function handleAddExercise() {
    var newExercise = wo.createExerciseViewModel();
    newExercise.fromJS({
        sets: [{ weight: 50, reps: 10 }]
    });
    newExercise.exerciseNumber = exercises().length  1;
    exercises.push(newExercise);

    wo.app.navigate("List/select/exercise", { direction: 'forward' });
} 

When you click an item in the Exercises list, the application gives a name to the newly created exercise and navigates to a view with the exercise details.

JavaScript
function handleItemClick(e) {
    var workout = wo.currentWorkout;
    if(!isEdit) {
        switch(key) {
            case "goal":
                workout.goal(e.itemData);
                workout.handleAddExercise();
                break;
            case "exercise":
                workout.currentExercise().name(e.itemData);
                wo.app.navigate("Exercise/add", { direction: "forward" });
                break;
        }
    }
};

The Search and Back functionalities are implemented for the "Select Goal" and "Select Exercise" views simultaneously. Refer to the description provided for the "Select Goal" view.

Exercise Details View

DXWorkout Home View

In this view, you can perform several sets for the current exercise, start another exercise or navigate to a view with the current results.

The "Exercise Details" view is created using the ViewModel returned by the Exercise function (see the Exercise.js file) and the "Exercise" HTML template (see the Exercise.html file).

The ViewModel of this view provides access to the fields of the current exercise's ViewModel.

  <!--JavaScript-->DXWorkout.Exercise = function(params) {
    //...
      return {
              //...
          workout: workout,
          exercise: exercise,
              //...
          viewShowing: function(args) {
              var currentWorkout = wo.currentWorkout,
                  currentExercise = currentWorkout.currentExercise(),
                  exerciseCount = currentWorkout.exercises().length;

              workout(currentWorkout);
              exercise(currentExercise);
              title('#' + exerciseCount + ' ' + currentExercise.name());
                    //...
              }
      };
  }

This allows for the adding and deleting of current exercise sets.

HTML
<div class="content edit-sets" data-bind="foreach: exercise().sets">
      <div>
            <div class="dx-field-label">Set <span data-bind="text: ($index()  1)"></span></div>
            <div class="dx-field-value">
                  <div data-bind="dxNumberBox: { value: reps, min: 1, placeholder: 'Reps' } "></div>
                  <span>&amp;times;</span>
                  <div data-bind="dxNumberBox: { value: weight, min: 0, placeholder: 'Weight' } "></div>
                  <span data-bind="text: weightUnit"></span>
                  <!-- ko if: canDelete() -->
                  <div class="minus" data-bind="dxAction: handleDelete" />
                  <!-- /ko -->
            </div>
      </div>
</div>
<div class="actions" data-bind="with: workout">
      <div data-bind="dxButton: { text: 'Add set', clickAction: handleAddSet }"></div>
      <!--...-->
</div>

As there can be many sets done, the list of sets is wrapped by the dxScrollView widget. This widget makes the content inside scrollable.

HTML
<div data-bind="dxScrollView: {}">
        <!--...-->
  </div>

To navigate to the next exercise, the handleAddExercise function of the current workout's ViewModel is called when clicking the "Next exercise" button. This function is also called when navigating from the Goals view to the Exercises view.

To cancel the current exercise or the current workout at this step, the "Cancel" dxCommand is implemented.

HTML
<div data-bind="dxCommand: { title: 'Cancel', location: 'back', id: 'back', action: handleCancel, visible: !DXWorkout.hardwareBackButton }"></div>

This command is not displayed on the devices that contain a hardware Back button. However, this command is executed in such devices anyway. Here, the same technique is used as for the hardware Back button in the Select Goal view.

JavaScript
DXWorkout.Exercise = function(params) {
    //...
    return {
        //The field to which the view's Cancel button is bound
        handleCancel: handleCancel,
        //The field to which the hardware Back button is bound
        backButtonDown: handleCancel,
        //...
    }
}

The handleCancel function makes the dxActionSheet widget visible.

JavaScript
function handleCancel() {
    commandsVisible(true);
}
HTML
<div data-bind="dxActionSheet: { title: 'Choose action', cancelText: 'Dismiss', dataSource: cancelCommands, visible: commandsVisible }"></div>

Here, "cancelCommand" is an array of commands that accomplish specified actions:

JavaScript
cancelCommands = [
    { text: "Cancel exercise", clickAction: cancelExercise },
    { text: "Cancel workout", clickAction: cancelWorkout }
];

Workout Results View

DXWorkout Home View

In this view, you can see all the information about the current workout. You can add notes here, and choose whether to continue this workout or finish it.

The "Workout Results" view is created using the ViewModel returned by the Results function (see the Results.js file) and the "Results" HTML template (see the Results.html file).

The ViewModel of this view provides access to the fields of the current workout's ViewModel.

JavaScript
DXWorkout.Results = function(params) {
    //...
      return {
          workout: workout,
        //...
      };
}

This allows the binding of the view's HTML elements to the workout's fields directly.

HTML
<!--ko with: workout -->
<div class="content">
    <!--...-->
    <div>
        <div class="dx-field-label">Workout goal</div>
        <div class="dx-field-value goal-caption" data-bind="text: goal" />
    </div>
    <div>
        <div class="dx-field-label">Total time</div>
        <div class="dx-field-value" data-bind="text: duration" />
    </div>
    <!--...-->
</div>

To display the workout date in the required format, the format function from the globalize library is used.

HTML
<div>
    <div class="dx-field-label">Workout date</div>
    <div class="dx-field-value" data-bind="text: Globalize.format(startDate(), 'MMM d, yyyy')" />
</div>

To add notes to the current workout, the dxTextArea widget is used.

HTML
<div class="large-container">
    <div class="dx-field-label">Notes</div>
    <div class="dx-field-value" data-bind="dxTextArea: { value: notes, rows: 3, placeholder: 'Notes', valueUpdateAction: handleNotesChange }"></div>
</div>

As in the "Exercise" view, the dxScrollable widget is used to allow end-users to scroll the view's content.

HTML
<div data-options="dxView : { name: 'Results', title: 'Workout Results' } " >
      <div data-options="dxContent : { targetPlaceholder: 'content' } " >
          <div data-bind="dxScrollView: {}">
              <!--...-->
          </div>
      </div>
  </div>                

Workout Logs

DXWorkout Home View

The "Logs" view is created using the ViewModel returned by the Log function (see the Log.js file) and the "Log" HTML template (see the Log.html file).

This view allows you to view a list of passed workouts grouped by months.

HTML
<div data-bind="dxList: { dataSource: log, grouped: true, itemClickAction: handleItemClick }">
    <div data-options="dxTemplate : { name: 'item' } " >
        <div class="log-list-item" data-bind="text: caption" />
    </div>
</div>

An item click navigates you to the Workout Results view showing the clicked workout.

JavaScript
function handleItemClick(e) {
    DXWorkout.app.navigate("Results/show/" + e.itemData.id);
};

DXWorkout Home View

As you can see, the Delete Workout button is displayed in a "danger" style. This style is set using the type option of the dxButton widget.

HTML
<!-- ko if: id -->
    <div data-bind="dxButton: { text: 'Delete workout', type: 'danger', clickAction: handleDelete }"></div>
<!-- /ko -->

Graphs View

DXWorkout Home View

The "Graphs" view is created using the ViewModel returned by the GoalGraphs function (see the GoalGraphs.js file) and the "GoalGraphs" HTML template (see the GoalGraphs.html file).

This view allows you to view a distribution between the passed workouts by goals. For this purpose, the dxPieChart widget from the ChartJS library is used.

HTML
<div id="goalsChart" data-bind="dxPieChart: goalsChartOptions"></div>
JavaScript
goalsChartOptions = {
    series: {
        argumentField: 'goal',
        valueField: 'count',
    },
    tooltip: {
        enabled: true,
        percentPrecision: 1,
        customizeText: function(value) {
            return value.percentText;
        }
    },
    legend: {
        horizontalAlignment: 'center',
        verticalAlignment: 'bottom',
        columnCount: 3,
        rowCount: 2
    },
    palette: 'Soft Pastel',
    dataSource: ko.observableArray([])
};                      

In addition, you can see a weight distribution during the passed workouts. To do this, switch to the Weight tab. The Goals and Weight tabs are displayed using the dxTabs widget.

HTML
<div id="tabs" data-bind="dxTabs: tabOptions"></div>
JavaScript
tabOptions = {
    items: [
        { text: "Goal" },
        { text: "Weight" }
    ],
    itemClickAction: function(value) {
        if (value.itemData.text === "Weight")
            DXWorkout.app.navigate("WeightGraphs", { root: "true" }); 
    },
    selectedIndex: selectedTab
};

As you can see in the code above, the application navigates to the "WeightGraphs" view when clicking the "Weight" tab. The "WeightGraphs" view is created using the ViewModel returned by the WeightGraphs function (see the WeightGraphs.js file) and the "WeightGraphs" HTML template (see the WeightGraphs.html file). This view is similar to the "Graphs" view except for the differenet chart widget and a combobox for choosing an exercise for the chart.

DXWorkout Home View

To dispay this chart, the dxChart widget from the ChartJS library is used.

HTML
<div id="weightChart" class="with-selects" data-bind="dxChart: {
    commonSeriesSettings: {
        argumentField: 'date'
    },
    series: [
        { valueField: 'weight', name: 'Weight' },
    ],
    argumentAxis: {
        grid: { visible: true },
        tickInterval: 'day',
        label: { format: 'monthAndDay' }
    },
    valueAxis: { min: 0 },        
    tooltip: {
        enabled: true,
        argumentFormat: 'shortDate',
        customizeText: function () {
            return this.seriesName + ': ' + this.valueText + '<br/>' + this.argumentText;
        }
    },
    legend: {
        verticalAlignment: 'bottom',
        horizontalAlignment: 'center'
    },
    dataSource: weightChartDataSource
}"></div>

The weightChartDataSource field, to which the chart's data source is bound, is updated when the combomox value is changed.

Settings View

DXWorkout Home View

This view is created using the ViewModel returned by the Settings function (see the Settings.js file) and the "Settings" HTML template (see the Settings.html file).

In this view, you can change units for weight and length. The new values will be saved to the data storage, as detailed above.

When clicking the "Goals" and "Exercises" list items, the application navigates you to the "List" view - the view that is used to select a goal or exercise while doing a workout.

HTML
<div  class="dx-field with-lookup-arrow" data-bind="event: { click: editGoals }">
    <div class ="dx-field-label">Goals</div>
    <div class="lookup-arrow"></div>
</div>  
<div class="dx-field with-lookup-arrow" data-bind="event: { click: editExercises }">
      <div class ="dx-field-label">Exercises</div>
      <div class="lookup-arrow"></div>
</div>
JavaScript
editGoals: function() {
    DXWorkout.app.navigate('List/edit/goal');
},
editExercises: function() {
    DXWorkout.app.navigate('List/edit/exercise');
}

As you can see, "edit" is passed as the action navigation parameter. This results in a change of the "List" view appearance.

DXWorkout Home View

The view's appearance is different because several HTML fields are related to the ViewModel's isEdit field. This field is calculated based on the value passed as the action navigation parameter.

JavaScript
DXWorkout.List = function (params) {
      var action = params.action,
          isEdit = action === "edit",
        //...
}

Find App in Stores

The DXWorkout application is available for download from Stores by the following links.