Vue.js Todo task with CSS animation and transition effect - javascript

How do you add a visual indicator such as a spinner and a tick when you save a new task in the example below.
Basically when I click the save button the following should happen:
Save button should become invisible
Cancel icon should become invisible, a spinner should appear instead to indicate something is happening
A tick icon should briefly appear if successfully saved and then disappear before displaying the delete icon.
The v-show that i'm using doesn't seem to work when I try to replicate a sleep effect to display the spinner when the updateTask() method is fired?
<div class="container" id="el">
<div class="row">
<div class="col-md-10"><h1>Tasks</h1></div>
<div class="col-md-2">
<button id="" class="btn btn-primary pull-right" type="button" v-on:click="createTask()">
Add New
</button>
</div>
</div>
<div class="row" >
<div class="col-sm-10 col-md-8">
<table class="table table-striped">
<tr class="row" v-for="task in tasks">
<td class="col-sm-5">
<div class="form-group">
<label class="sr-only" for="name">
Name</label>
<input v-if="editKey == task.id" name="name" type="text" class="form-control" v-model="task.name">
<span v-else v-on:click="editTask(task)">{{ task.name }}</span>
</div>
</td>
<td class="col-sm-5">
<div class="form-group">
<label class="sr-only" for="date">
Date</label>
<input v-if="editKey == task.id" name="date" type="text" class="form-control date-picker" v-pikaday="task.date">
<span v-else v-on:click="editTask(task)">{{ task.date }}</span>
</div>
</td>
<td class="col-sm-2">
<ul class="list-inline">
<li v-if="editKey == task.id" >
<button class="btn btn-success btn-sm" type="button" v-on:click="updateTask(task)" v-show="!loading">
Save
</button>
</li>
<li v-if="editKey == task.id ">
<span v-show="!loading"><i class="fa fa-times text-danger" v-on:click="cancelEdit(task)" title="Cancel"></i></span>
<span v-show="loading"> <i class="fa fa-spinner"></i></span>
</li>
<li v-if="editKey !== task.id">
<i class="fa fa-trash-o text-muted" v-on:click="removeTask(task)" title="Delete"></i>
</li>
<li v-if="editKey !== task.id && task.id == -1">
<i class="fa fa-exclamation-triangle text-warning" title="Unsaved"></i>
</li>
</ul>
</td>
</tr>
</table>
</div>
<pre>{{$data | json }}</pre>
</div>
</div>
<script>
Vue.directive('pikaday', {
twoWay: true,
bind: function () {
var self = this
$(this.el)
.pikaday({
format: 'D MMM YYYY',
defaultDate: moment().toDate()
})
.on('change', function () {
self.set(this.value)
})
},
update: function (value) {
$(this.el).val(value).trigger('change')
},
unbind: function () {
$(this.el).off().pikaday('destroy')
}
})
var vm = new Vue({
el: '#el',
data: {
editKey: '',
loading: false,
beforeEditCache: {
id: '',
name: '',
date: ''
},
editedTask: null,
tasks: [
{id: 1, name: 'Task A', date: '25 Dec 2015'},
{id: 2, name: 'Task B', date: '26 Dec 2015'}
]
},
methods: {
createTask: function() {
// if previously we were editing a task, lets cancel the edit
if (this.editedTask){
this.cancelEdit();
}
// add new task with id -1 to indicate it hasn't been persisted
this.tasks.push({
id: -1,
name: '',
date: ''
});
// set edit key
this.editKey = -1;
},
storeTask: function(task) {
// check if mandatory field completed
if (!task.name || !task.date) {
return;
}
// persist the task by generating valid id
task.id = Math.floor((Math.random() * 100) + 1);
},
editTask: function(task) {
// if we were previously editing a task and clicked on another to edit without saving, let cancel the edit
if (this.editedTask){
this.cancelEdit();
}
this.setBeforeEditCache(task);
this.editedTask = task;
this.editKey = task.id;
},
updateTask: function (task) {
// if its a new task
if (task.id == -1){
this.storeTask(task);
}
// otherwise we are editing an existing task
else {
if (!this.editedTask.name || !this.editedTask.date) {
return;
}
this.loading = true;
this.sleep(3000);
this.editedTask = null;
this.editKey = '';
this.loading = false;
}
},
cancelEdit: function (task = null) {
if (task && task.id == -1) {
this.removeTask(task);
}
else {
this.editedTask.name = this.beforeEditCache.name;
this.editedTask.date = this.beforeEditCache.date;
this.editedTask = null;
this.editKey = '';
}
},
removeTask: function(task) {
this.tasks.$remove(task);
},
setBeforeEditCache: function(task) {
this.beforeEditCache.id = task.id;
this.beforeEditCache.name = task.name;
this.beforeEditCache.date = task.date;
},
sleep: function(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}
}
})
</script>
This is the fiddle https://jsfiddle.net/ozzii/6oe2k3py/
* UPDATE *
So I've managed to update this - see the new fiddle, to provide the functionality required. But it's a very messy solution - code is all over the place. Anyone know of a better/cleaner way to refactor and achieve the same functionality and perhaps provide a fade in/out affect to the tick when you save the element?

Here is the part about the sleep effect. You can use setTimeout and use the bind function to make sure the this context inside it is the Vue component.
this.loading = true;
setTimeout((function(){
this.editedTask = null;
this.editKey = '';
this.loading = false;
}).bind(this), 3000);

Related

How to dynamically create Input Fields in VueJS

I'm trying to dynamically add or remove input fields on the go.
I got a simple solution from here https://smarttutorials.net/dynamically-add-or-remove-input-textbox-using-vuejs/, which works. However saving input values in the database stops it's functionality.
Component Code:
<div class="form-group" v-for="(input,k) in inputs" :key="k">
<input type="text" class="form-control" v-model="input.name" />
<input type="text" class="form-control" v-model="input.party" />
<span>
<i
class="fas fa-minus-circle"
#click="remove(k)"
v-show="k || ( !k && inputs.length > 1)"
></i>
<i
class="fas fa-plus-circle"
#click="add(k)"
v-show="k == inputs.length-1"
></i>
</span>
</div>
<button #click="addCandidate">
Submit
</button>
<script>
export default {
data() {
return {
inputs: [
{
name: "",
party: ""
}
]
};
},
methods: {
add(index) {
this.inputs.push({ name: "", party: "" });
},
remove(index) {
this.inputs.splice(index, 1);
},
addCandidate() {
axios
.post("/candidates", this.inputs)
.then(response => {})
.catch(error => {});
}
}
};
</script>
I always get a 422 error, saying the input field is empty.
This is not a Vue problem. Before you send an array you'll need to call JSON.stringify() on it, then decode it on the server with Laravel. Example:
foreach(json_decode($request -> input('my_prop_name ')) as $my_object_in_array)
{
print_r($my_object_in_array); // this is your object name/party
}
Vue code working like magic. :)
new Vue({
el: '#app',
data () {
return {
inputs: [{
name: '',
party: ''
}]
}
},
methods: {
add () {
this.inputs.push({
name: '',
party: ''
})
console.log(this.inputs)
},
remove (index) {
this.inputs.splice(index, 1)
},
addCandidate () {
axios
.post('/candidates', {
my_prop_name: JSON.stringify(this.inputs)
})
.then(response => {})
.catch(error => {})
}
}
})
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id=app>
<div class="form-group" v-for="(input,k) in inputs" :key="k">
<input type="text" class="form-control" v-model="input.name">
<input type="text" class="form-control" v-model="input.party">
<span>
<i class="fas fa-minus-circle" #click="remove(k)" v-show="k || ( !k && inputs.length > 1)">Remove</i>
<i class="fas fa-plus-circle" #click="add(k)" v-show="k == inputs.length-1">Add fields</i>
</span>
</div>
<button #click="addCandidate">
Submit
</button>
</div>

Unable to load a Knockout Component - Unknown template value: [object Object]

This is how I am using the component and sending the parameters using a Custom Element:
<div class="container" data-bind="with: DevoteeList">
<div class="row" style="padding: 10px;">
<div class="col-md-8"></div>
<div class="col-md-4">
<ko-pager params="Data: Devotees,
Modifier: $parent.DevoteeModifier,
PageCount: DevoteesPageCount(),
Url: '#Url.Action("SelectDevotees", "Devotee", new { a = 1 })'"></ko-pager>
</div>
</div>
This is how I am defining a Knockout Component. It is a Pager that I want to use at few places. But, I am receiving the error: Uncaught Error: Unable to process binding "with: function (){return SelectDevotees }"
Message: Unable to process binding "with: function (){return DevoteeList }"
Message: Unable to process binding "component: function () { return l }"
Message: Component 'ko-pager': Unknown template value: [object Object]
ko.components.register('ko-pager', {
viewModel: function (params) {
var self = this;
self.currentPage = ko.observable(1);
self.pages = ko.observableArray([]);
self.PageCount = ko.observable(params.PageCount);
//self.currentPage.subscribe(function (nv) {
// self.GetPage(self.parent);
//});
self.GetPages = function () {
for (var i = 1; i <= params.PageCount ; i++) {
self.pages.push(i);
}
return self.pages;
};
self.FirstPage = function () {
self.GetPage(1);
};
self.PrevPage = function () {
if (self.currentPage() > 1) {
var pn = self.currentPage() - 1;
self.GetPage(pn);
}
};
self.LastPage = function () {
self.GetPage(params.PageCount);
};
self.NextPage = function () {
if (self.currentPage() < params.PageCount) {
var pn = self.currentPage() + 1;
self.GetPage(pn);
}
};
self.GetPage = function (pg) {
if (pg == null)
pg = self.currentPage();
else
self.currentPage(pg);
var url = params.Url + '&pageNumber=' + pg;
$.get(url, function (data) {
var t = ko.mapping.fromJS(data);
if (params.Modifier) {
params.Modifier(t);
}
params.Data(t());
});
};
},
template: { element: document.getElementById('ko-ajax-pager') }
});
<div id="ko-ajax-pager" style="display: none;">
<div class="row" style="padding: 10px;" data-bind="visible: PageCount > 1">
<div class="col-md-1"></div>
<div class="col-md-2">
<input type="button" value="First" class="btn" data-bind="click: FirstPage" />
</div>
<div class="col-md-2">
<input type="button" value="Prev" class="btn" data-bind="click: PrevPage" />
</div>
<div class="col-md-2">
<select data-bind="options: GetPages(), value: currentPage, event: { change: GetPage(null) }">
</select>
</div>
<div class="col-md-2">
<input type="button" value="Next" class="btn" data-bind="click: NextPage" />
</div>
<div class="col-md-2">
<input type="button" value="Last" class="btn" data-bind="click: LastPage" />
</div>
<div class="col-md-1"></div>
</div>
</div>
Can someone please figure out, what is wrong?

Angular JS Filter - Filter by 3 checkboxes

I have a sales array and I want to filter by the status of the sale action (finished, pending or/and failed). What I am trying is to show entire list, and unchecking the checkboxes some rows will disapear.
HTML Code
<div class="row" ng-repeat="sale in salesArray | filter: okStatus | filter: pendingStatus | filter: failedStatus">
<div class="col-lg-1 col-md-1 col-sm-1 col-xs-1">
<i ng-class="'icon ' + sale.icon + ( sale.status == 'ok' ? ' text-green' : (sale.status == 'pending' ? ' text-amber' : ' text-red') )"></i>
</div>
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2">
<|sale.user|>
</div>
<div class="col-lg-3 col-md-3 col-sm-3 col-xs-3">
<|sale.product|>
</div>
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2">
<|sale.price | currency:"$"|>
</div>
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2">
<|sale.date|>
</div>
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2">
</div>
</div>
<ul class="justified-list">
<li>
<div class="checkboxer checkboxer-green form-inline">
<input type="checkbox" id="checkboxColor10" ng-model="okStatus" ng-value="ok">
<label for="checkboxColor10">Finalizados (<|(salesArray | filter: {status: 'ok'}).length|>)</label>
</div>
</li>
<li>
<div class="checkboxer checkboxer-amber form-inline">
<input type="checkbox" id="checkboxColor14" ng-model="pendingStatus" ng-value="pending">
<label for="checkboxColor14">En proceso (<|(salesArray | filter: {status: 'pending'}).length|>)</label>
</div>
</li>
<li>
<div class="checkboxer checkboxer-red form-inline">
<input type="checkbox" id="checkboxColor1" ng-model="failedStatus" ng-value="failed">
<label for="checkboxColor1">Abortados (<|(salesArray | filter: {status: 'failed'}).length|>)</label>
</div>
</li>
</ul>
Controller Code:
$scope.okStatus = "";
$scope.pendingStatus = "";
$scope.failedStatus = "";
$scope.salesArray = [
{icon: "ion-checkmark-round", user: "Jtd", price: 123.32, product: "Sesión de una hora", date: "12/02/2015", status: "ok"},
{icon: "ion-close-round", user: "Tar", price: 53.00, product: "Sesión de media hora", date: "14/02/2016", status: "failed"},
{icon: "ion-compass", user: "Rao", price: 103.90, product: "Sesión de 45 minutos", date: "15/03/2016", status: "pending"}
];
How can I get this filter? Is obvious now it is not working
I combined knowledge from several other SO questions to suggest a solution:
Bind all checkboxes to a single model
Use a custom filter to filter the sales according to the checkboxes model
The solution can look like this (better to look at the plunkr):
HTML
<body ng-app="app" ng-controller="ctrl">
<label ng-repeat="status in statuses">
<input
type="checkbox"
name="selectedStatuses[]"
value="{{status}}"
ng-checked="selection.indexOf(status) > -1"
ng-click="toggleSelection(status)"
> {{status}}
</label>
<div ng-repeat="sale in salesArray | selectedFilter:selection">{{ sale.user }} | {{sale.product}} | {{sale.status}}</div>
</body>
JS
app = angular.module('app', []);
app.filter('selectedFilter', function() {
return function(items, options) {
var filtered = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (options.indexOf(item.status) != -1) {
filtered.push(item);
}
}
return filtered;
};
});
app.controller('ctrl', ['$scope', function($scope) {
$scope.statuses = ['ok', 'pending', 'failed'];
$scope.selection = ['ok', 'pending', 'failed'];
// toggle selection for a given fruit by name
$scope.toggleSelection = function toggleSelection(status) {
var idx = $scope.selection.indexOf(status);
// is currently selected
if (idx > -1) {
$scope.selection.splice(idx, 1);
}
// is newly selected
else {
$scope.selection.push(status);
}
};
$scope.salesArray = [{
icon: "ion-checkmark-round",
user: "Jtd",
price: 123.32,
product: "Sesión de una hora",
date: "12/02/2015",
status: "ok"
}, {
icon: "ion-close-round",
user: "Tar",
price: 53.00,
product: "Sesión de media hora",
date: "14/02/2016",
status: "failed"
}, {
icon: "ion-compass",
user: "Rao",
price: 103.90,
product: "Sesión de 45 minutos",
date: "15/03/2016",
status: "pending"
}];
}]);
if the value of the ng-class is expression, then it should be placed {}.
Make it like switch case. For example 'class to be applied':'the condition' with separated commas as shown below..
HTML Code:
<i ng-class="{'text-green':(sale.status == 'ok'),'
text-amber':(sale.status == 'pending'), 'text-red':(sale.status != 'pending')}"></i>
The logic be could written in function and the method could be called similar to below. Ensure construct the return structure as you want.
HTML Code:
<i ng-class="getDisplayClass(sale.icon, sale.status)"></i>
Controller Code:
$scope.getDisplayClass = function(saleicon, status) {
if (status == 'ok')
return 'icon' + saleicon + 'text-green';
else if (status == 'pending')
return 'icon' + saleicon + 'icon text-amber';
else
return 'icon' + saleicon + 'icon text-red';
}

Directive Does Not Inherit Controller's Scope

I have a controller set up which contains a few functions to do form validation. In this controllers $scope I have an array $scope.errorMsgs that is populated with strings of error messages the user makes when filling out the form.
I also have a directive that displays this form. They are both attached to the same module. The controller and directive are in separate files within the same directory. However the scope in the directive's link function does not reference the $scope in the controller. They have different $ids in fact. Any ideas as to why this is happening?
Module:
angular.module('aop.availabilitysolver', [
'aop.services',
'checklist-model'
]).run(['widgetService', function(widgetService) { 'use strict';
widgetService.registerWidgets([
{
title: 'AVAILABILITYSOLVER.WIDGETS.AVAILABILITYSOLVER',
translatableDescription: 'AVAILABILITYSOLVER.WIDGETS.AVAILABILITYSOLVER',
type: 'aop-availability-solver',
configuration: {},
width: 1
}
]);
}]);
Controller
angular.module('aop.availabilitysolver')
.controller('AvailabilitySolverController', ['$scope',
function ($scope) { 'use strict';
//console.log($scope);
$scope.selectGoalDropdown = ['Impressions', 'Spend'];
$scope.selectGoalTimespan = ['Day', 'Week', 'Month'];
$scope.selectGoals = [
{
id: '1',
name: 'Impressions'
},
{
id: '2',
name: 'Spend'
}
];
$scope.selectTimespan = [
{
id: '4',
name: 'Day'
},
{
id: '5',
name: 'Week'
},
{
id: '6',
name: 'Month'
}
];
$scope.selectedItem = 'test2';
$scope.selectedItem1 = 'test3';
$scope.per = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20];
$scope.hours = [1, 3, 4, 12, 24, 36, 48, 72, 120, 168];
$scope.uncpd = ['uncapped'];
$scope.times = {
amt: [],
freq: [],
uncapped: []
};
$scope.caps = [];
$scope.quantity;
$scope.level = 'Fcap';
$scope.showErrors = false;
$scope.errorMsgs = [];
$scope.calculateFreqCaps = function(amt, freq, uncap) {
var rtn = [];
function isNothingSelected() { // No selections made
if(amt.length === 0 && freq.length === 0 && uncap.length === 0) {
rtn.push('Please choose frequency settings or select \'uncapped.\'');
$scope.errorMsgs.push('Please choose frequency settings or select \'uncapped.\'');
}
}
function malformedFrequencyOpts() { // Selected amount but no frequency & vice versa
if((amt.length > 0 && freq.length === 0) || (amt.length === 0 && freq.length > 0)) {
rtn.push('Please select both an amount and a frequency.');
$scope.errorMsgs.push('Please select both an amount and a frequency.');
}
}
function selectedTooMuch() { // Uncapped and frequency settings selected
if((amt.length > 0 || freq.length > 0) && uncap.length === 1) {
rtn.push('Choose uncapped only if no amount or frequency is selected');
$scope.errorMsgs.push('Choose uncapped only if no amount or frequency is selected');
}
}
isNothingSelected();
malformedFrequencyOpts();
selectedTooMuch();
if(rtn.length > 0) {
return rtn;
} else if (amt.length === 0 && freq.length === 0 && uncap.length === 1) { // Only uncapped selected
return ['uncapped'];
}
angular.forEach(amt, function (a) {
angular.forEach(freq, function (f) {
rtn.push(a + '/' + f + 'h');
});
});
return rtn;
};
$scope.validateSelectGoalQuantity = function(n) {
if(!Number(n)) {
$scope.errorMsgs.push('Quantity must be a number');
return false;
}
return true;
};
$scope.submitBtnClick = function() {
// Build Frequency cap JSON object
$scope.caps = $scope.calculateFreqCaps($scope.times.amt, $scope.times.freq, $scope.times.uncapped);
$scope.validateSelectGoalQuantity($scope.quantity);
if($scope.errorMsgs.length > 0) {
console.log($scope.errorMsgs);
// Show error message div and clear $scope.errorMsgs
$scope.showErrors = true;
//$scope.errorMsgs.length = 0;
}
else {
$scope.showErrors = false;
}
};
}]);
Directive
angular.module('aop.availabilitysolver')
.directive('aopAvailabilitySolver', ['$filter', function($filter) { 'use strict';
return {
restrict: 'E',
link: function(scope, element, attrs) {
angular.noop(attrs);
// Hide error div initially
$('.availabilitySolverErrorDisplay').hide();
var i = element.find('.levelRadio :radio');
i.on('click', function() {
if($(i[0]).prop('checked')) {
scope.level = 'Fcap';
element.find('.freqCapDiv').show();
}
else {
scope.level = 'Bid';
element.find('.freqCapDiv').hide();
}
});
console.log(scope);
},
templateUrl: 'features/availabilitySolver/availabilitySolver.html'
};
}]);
HTML
<div ng-controller="AvailabilitySolverController">
<div class="container-fluid availabilitySolverScreen1">
<div class="row">
<div class="alert alert-danger availabilitySolverErrorDisplay">
</div>
</div>
<div class="row">
Campaign Selector
</div>
<!-- Goals -->
<div class="row">
<h5>Select Goal</h5>
<form name="selectGoalForm" novalidate>
<div class="col-md-4">
<button name="goals" class="btn btn-default" ng-model="selectedItem" ng-options="value.id as value.name for (key, value) in selectGoals" data-style="btn-primary" bs-select></button>
</div>
<div class="col-md-4">
<div class="col-md-10">
<input type="number" name="quantity" class="form-control" placeholder="Quantity" ng-model="quantity">
</div>
<div class="col-md-2">
per
</div>
</div>
<div class="col-md-4">
<button name="timespan" class="btn btn-default" ng-model="selectedItem1" ng-options="value.id as value.name for (key, value) in selectTimespan" data-style="btn-primary" bs-select></button>
</div>
</form>
</div><!-- End goals -->
<!-- Level cap -->
<div class="row">
<h5>Level</h5>
<div class="col-md-12">
<form class="levelRadio">
<input name="level" value="Fcap" type="radio" checked> Fcap
<input name="level" value="Bid" type="radio"> Bid
</form>
</div>
</div><!-- end level cap -->
<!-- Frequency cap analysis -->
<div class="row freqCapDiv">
<h5>Customize Frequency Cap for Analysis</h5>
<div class="col-md-8">
<!-- per -->
<div class="col-md-4">
<ul>
<li ng-repeat="item in per">
<input type="checkbox"
checklist-value="item"
checklist-model="times.amt" /> {{item}} per
</li>
</ul>
</div><!-- end per -->
<!-- hour(s) -->
<div class="col-md-4">
<ul>
<li ng-repeat="item in hours">
<input type="checkbox"
checklist-value="item"
checklist-model="times.freq" /> {{item}} hr
</li>
</ul>
</div>
<!-- uncapped -->
<div class="col-md-4">
<ul>
<li ng-repeat="item in uncpd">
<input type="checkbox"
checklist-value="item"
checklist-model="times.uncapped" /> uncapped
</ul>
</div>
</div>
<div class="col-md-4">
</div>
</div><!-- end frequency cap analysis -->
<!-- submit button -->
<div class="row">
<button class="btn btn-primary" ng-click="submitBtnClick()">Submit</button>
</div>
</div>
</div>

$parent.myFunction not returning row as parameter in KnockoutJs

I'm using knockoutjs for the first time in an attempt to have a list of tasks and then to be able to click on one to show it in a popup form. The issue I'm having is with data-bind="click: $parent.edit", which calls ToDoListViewModel/edit.
When I first started writing the list code, it would pass in the current row's todo object as the first parameter into the edit function. After adding the second view model for my add/edit popup form, it no longer behaves this way. Calling $parent.edit still calls the edit function, however now it passes in an empty object { }.
It seems like I'm running into some sort of conflicting bindings issue, any ideas?
Here is the to do list html:
<table class="table table-striped table-hover" data-bind="visible: isVisible">
<thead>
<tr>
<th>To-Do</th>
<th>Estimate</th>
<th>Deadline</th>
#if (Model == null) { <th>Property</th> }
<th>Tenant</th>
<th style="display: none;">Assigned To</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: todos">
<tr data-bind="visible: isVisible()">
<td><a class="pointer" title="Edit To-Do" data-bind="click: $parent.edit, text: Task"></a></td>
<td data-bind="text: FormattedAmount"></td>
<td data-bind="text: FormattedDueDate"></td>
<td data-bind="visible: $parent.showPropertyColumn, text: PropertyName"></td>
<td data-bind="text: TenantName"></td>
<td>
<div class="btn-group pull-right">
<a class="btn btn-primary pointer default-click-action" data-bind="click: $parent.markComplete">Mark Complete</a>
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a class="pointer" data-bind="click: $parent.edit">Edit To-Do</a></li>
<li><a class="pointer" data-bind="click: $parent.remove">Delete To-Do</a></li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
And here is the popup html:
#model Housters.Schemas.Models.Property.Listing
<div id="popup" class="modal fade" style="display: none;" data-bind="with: form">
#using (Html.BeginForm("SaveTodo", "Properties", FormMethod.Post, new { Id = "form", #class = "form-horizontal" }))
{
<div class="modal-header">
<a class="close" data-dismiss="modal">×</a>
<h3>To-Do Information</h3>
</div>
<div class="modal-body">
<input id="id" name="id" type="hidden" data-bind="value: todo().Id" />
<input id="originalListingId" type="hidden" value="#(Model != null ? Model.IdString : "")" />
<div class="control-group #(Model != null ? " hide" : "")">
<label class="control-label" for="listingId">Property</label>
<div class="controls">
#Html.DropDownList("listingId", ViewBag.Listings as SelectList, "None",
new Dictionary<string, Object> { { "data-bind", "value: todo().ListingId" } })
</div>
</div>
<div class="control-group">
<label class="control-label" for="tenantId">Tenant</label>
<div class="controls">
<select id="tenantId" name="tenantId" class="span11" data-bind="value: todo().TenantId">
<option value="">None</option>
</select>
<p class="help-block">Is this task related to a certain tenant?</p>
</div>
</div>
<div class="control-group">
<label class="control-label" for="amount">Estimate to Complete</label>
<div class="controls">
<div class="input-prepend"><span class="add-on">$</span><input type="text" id="amount" name="todo.Amount" maxlength="10"
class="span11 number" data-bind="value: todo().Amount" /></div>
</div>
</div>
<div class="control-group">
<label class="control-label" for="dueDate">Due Date</label>
<div class="controls">
<input type="text" id="dueDate" name="todo.DueDate" maxlength="10"
class="span11 date" data-bind="value: todo().DueDate" />
</div>
</div>
<div class="control-group">
<label class="control-label" for="task">Task<span class="required-asterisk">*</span></label>
<div class="controls">
<textarea id="task" name="todo.Task" rows="4" class="span11 required" data-bind="value: todo().Task"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn pointer" data-dismiss="modal">Close</a>
<a id="mark-complete-popup" class="btn btn-primary" data-dismiss="modal" data-bind="visible: (todo().Id && !todo().IsComplete), click: markComplete">Mark Complete</a>
<button type="button" class="btn btn-primary" data-bind="click: save">Save</button>
</div>
}
</div>
And lastly, here is the javascript defining the view models:
function ToDoList () {
if(!(this instanceof arguments.callee))
return new arguments.callee();
var parent = this;
this.showCompleted = ko.observable(false);
this.tenantFilter = new PropertyTenantFilter();
this.viewModel = {
list: new ToDoListViewModel(),
form: new ToDoFormViewModel()
};
this.init = function () {
//get all tenants.
utils.block($("#grid-content"), "Loading");
this.tenantFilter.init(function () {
//initialize view model.
ko.applyBindings(this.viewModel);
//setup controls & events.
$("#dueDate").datepicker();
$("#listingId").change(this.tenantFilter.getByListing.bind(this.tenantFilter)).change();
} .bind(this));
};
function ToDoListViewModel() {
//init.
var self = this;
self.todos = ko.observableArray([]);
//computed.
self.showPropertyColumn = ko.computed(function () {
return $("#originalListingId").val().length == 0;
});
self.isVisible = ko.computed(function () {
return _.find(self.todos(), function (todo) { return todo.isVisible(); }) != null;
});
//operations.
self.add = function () {
//set form field values.
parent.viewModel.form.fill(new schemas.ToDo({}, parent));
//show popup.
$("#popup").modal("show");
};
self.edit = function (todo) {
console.debug("edit: " + JSON.stringify(todo));
//set form field values.
parent.viewModel.form.fill(todo);
//update tenants dropdown for selected listing.
parent.tenantFilter.getByListing();
//show popup.
$("#popup").modal("show");
};
self.markComplete = function (todo) {
parent.markComplete(todo);
};
self.remove = function (todo) {
var result = confirm("Are you sure that you want to delete this To-Do?");
if (result) {
//save changes.
utils.ajax(basePath + "properties/deletetodo",
{ id: todo.Id },
function (success) {
//refresh results.
self.todos.remove(todo);
//show result.
utils.showSuccess('The To-Do has been deleted successfully');
}
);
}
};
self.toggleShowCompleted = function () {
parent.showCompleted(!parent.showCompleted());
$("#showCompletedTodos").text(parent.showCompleted() ? "Show Active" : "Show Completed");
};
self.update = function (todo) {
var existingToDo = _.find(self.todos(), function (item) { return item.Id() == todo.Id(); });
existingToDo = todo;
};
//load todos from server.
utils.ajax(basePath + "properties/gettodos",
{ id: $("#originalListingId").val(), showCompleted: parent.showCompleted() },
function (results) {
var mappedTodos = $.map(results, function (item) { return new schemas.ToDo(item, parent); });
self.todos(mappedTodos);
$("#grid-content").unblock();
}
);
}
function ToDoFormViewModel() {
//init.
var self = this;
self.todo = ko.observable({});
utils.setupPopupForm(self.saved);
//operations.
self.fill = function (todo, isEdit) {
//set form field values.
self.todo = todo;
if (todo.Id()) {
//update tenants dropdown for selected listing.
parent.tenantFilter.getByListing();
}
//show popup.
$("#popup").modal("show");
};
self.save = function (todo) {
self.todo = todo;
$("#form").submit();
};
self.saved = function (result) {
var todo = new schemas.ToDo(result.todo, parent);
if (result.isInsert)
parent.viewModel.list.todos().push(todo);
else
parent.viewModel.list.update(todo);
utils.showSuccess("Your To-Do has been saved successfully.");
};
self.markComplete = function (todo) {
parent.markComplete(todo);
};
}
this.markComplete = function (todo) {
var result = confirm("Are you sure that you want to mark this To-Do complete?");
if (result) {
//save changes.
utils.ajax(basePath + "properties/marktodocomplete", {
id: todo.Id()
},
function () {
todo.IsComplete(true);
//show success.
utils.showSuccess('Your To-Do has been marked completed');
} .bind(this)
);
}
}
this.init();
}
One possible solution is to replace:
data-bind="click: $parent.edit"
with
data-bind="click:$root.viewModel.list.edit"

Categories

Resources