I want to use a select x-editable in my Meteor application. My goal is to assign users to groups. This should be reactive, so when you assign a user, other clients should see the changes. The current problem is that the assignment works (data-value changes), but only the user who made the change is able to see the new value.
Here is my code:
Template.userGroup.rendered = function() {
var groupId = this.data._id;
var sourceUsers = [];
Users.find().forEach(function(user) {
sourceUsers.push({value: user._id, text: user.username});
});
Tracker.autorun(function() {
$('.assign-user').editable("destroy").editable({
emptytext: "Empty",
source: sourceUsers,
success: function(response, result) {
if (result) {
Groups.update({_id: groupId}, {$set: {adminId: result}});
}
}
});
});
};
<template name="userGroup">
</template>
I already tried to "destroy" the stale x-editable and put it inside the Tracker.autorun function, but unfortunately, this does not work.
Any help would be greatly appreciated.
I don't use Tracker.autorun but I use x-editable for inline editing like this:
(also used it for group assigments - just like your case, but found it too clumsy on the UI side). Anyway, here's my code:
Template
<template name="profileName">
<td valign='top'>
<div id="profileNameID" class="editable" data-type="text" data-rows="1">{{profile.name}}</div>
</td>
</template>
And on the JS side
Template.profileName.rendered = function () {
var Users = Meteor.users;
var container, grabValue, editableColumns, mongoID,
_this = this;
var container = this.$('#profileNameID');
var editableColumns = container.size();
grabValue = function () {
var gValue = $.trim(container.html());
return gValue;
};
$.fn.editable.defaults.mode = 'inline';
return container.editable({
emptytext: 'Your name goes here',
success: function (response, newValue) {
var mongoID = removeInvisibleChars($(this).closest("tr").find(".mongoid").text());
var editedUser = _users.findOne({
_id: mongoID
});
Meteor.users.update(mongoID, {
$set: {
"profile.name": newValue
}
});
return container.data('editableContainer').formOptions.value = grabValue;
}
});
Update happens immediately on all subscribed authorized clients.
Related
I am using bootstrap typeahead (GitHub) to create search forms. After user inputs something I am doing elasticsearch query using elasticsearch.js and returning results. The problem is that results displayed in typeahead are always one character behind, not suggesting correct values.
Typeahead input:
<input type="text" data-provide="typeahead" class="form-control typeahead" id="searchInputId" placeholder="Search" autocomplete="off">
Here is my code:
var elasticsearchAddress = "exampleserver.com:9200";
var elasticsearchClient = createElasticsearchClient(elasticsearchAddress);
var data = [];
$("#searchInputId").typeahead({ source:data, items:10, fitToElement:true });
$("#searchInputId").on("input", function(){
var searchTerm = $("#searchInputId").val();
elasticsearchMathPhrasePrefixSearch(elasticsearchClient, searchTerm, function () {
$("#searchInputId").data('typeahead').source = getElasticsearchSearchResultsArray();
});
});
elasticsearchMathPhrasePrefixSearch() function
function elasticsearchMathPhrasePrefixSearch(client, searchPhrase, callback) {
console.log("Searching for: " + searchPhrase);
client.search({
body: {
"query": {
"match_phrase_prefix": {
"accountName": searchPhrase
}
}
}
}, function (error, response) {
if (error) {
console.trace('ELASTICSEARCH: Search query failed');
} else {
console.log('ELASTICSEARCH: Search query OK');
var doc = response.hits.hits;
elasticsearchSearchResultsArray = getDocs(doc);
}
callback();
});
}
getDocs() function
function getDocs(doc){
var searchResultsArray=[];
for(var i = 0; i < doc.length; i++){
searchResultsArray.push(doc[i]._source.accountName);
}
return searchResultsArray;
getElasticsearchSearchResultsArray() function
function getElasticsearchSearchResultsArray(){
return elasticsearchSearchResultsArray;
}
elasticsearchSearchResultsArray is a global array that holds the results. Because of the JS async nature I had no other idea to make it work.
EDIT:
Ok, I modified my code so the source is updated correctly with help of this Issue #1997. But now I have got another problem. The typeahead dropdown is not displayed when I type.
My new code:
var empty = [];
$("#searchInputId").typeahead({ source:empty, items:10, fitToElement:true });
$("#searchInputId").on("keyup", function(ev){
ev.stopPropagation();
ev.preventDefault();
//filter out up/down, tab, enter, and escape keys
if( $.inArray(ev.keyCode,[40,38,9,13,27]) === -1 ){
var self = $(this);
//set typeahead source to empty
self.data('typeahead').source = [];
//active used so we aren't triggering duplicate keyup events
if( !self.data('active') && self.val().length > 0){
self.data('active', true);
//Do data request. Insert your own API logic here.
var searchTerm = self.val();
elasticsearchMathPhrasePrefixSearch(elasticsearchClient, searchTerm, function() {
//set this to true when your callback executes
self.data('active',true);
//set your results into the typehead's source
self.data('typeahead').source = getElasticsearchSearchResultsArray();
//trigger keyup on the typeahead to make it search
self.trigger('keyup');
//All done, set to false to prepare for the next remote query.
self.data('active', false);
});
}
}
});
Try to use on("keyup") instead. input is 1 character behind.
Ok, I resolved it myself. I switched from bootstrap3-typeahead to jQuery UI Autocomplete. It is working great and the script is much smaller.
New code:
$("#searchInputId").on("keydown", function () {
$("#searchInputId").autocomplete({
source: function(request, response) {
var searchTerm = $("#searchInputId").val();
elasticsearchMathPhrasePrefixSearch(elasticsearchClient, searchTerm, function (){
response(getElasticsearchSearchResultsArray());
});
}
});
});
I'm at my wits end! In angular I've got a controller and a view.
There are 2 dropdowns on the page which need to reset to default once the restart button has been clicked.
I can set the value of the boxes as they render by pushing a "select" option into the collection inside the controller. However, when the reset button is pressed, which runs the init() method again, the dropdowns should be set back to the first value. This doesn't occur, the values for $scope.selectedYear and $scope.selectedReport remain as they did before the reset button was pressed.
This is the full code for the controller
function TreeReportsCHLController($scope, $q, $routeParams, reportsDashboardResource, navigationService, notificationsService, dialogService, entriesManageDashboardResource, $timeout) {
// Methods
var generalError = function () {
notificationsService.error("Ooops", "There was an error fetching the data");
$scope.actionInProgress = false;
}
// Scope
$scope.selectedYear = "";
$scope.init = function () {
$scope.hasCompleted = false;
$scope.actionInProgress = false;
$scope.yearSelected = false;
$scope.reportTypes = ["Choose", "Entered", "ShortListed", "Winner", "Recieved"];
$scope.selectedReport = "";
$scope.entryYears = new Array();
$scope.entryYears.push("Choose a Year");
entriesManageDashboardResource.getEntryYears().then(function (response) {
angular.forEach(response, function (value, key) {
$scope.entryYears.push(value);
});
});
$scope.selectedYear = $scope.entryYears[0];
$scope.selectedReport = $scope.reportTypes[0];
};
$scope.yearHasSelected = function(selectedYear) {
$scope.yearSelected = true;
$scope.selectedYear = selectedYear;
};
$scope.generateFile = function (selectedReport) {
$scope.actionInProgress = true;
var reportDetail = {
entryYear: $scope.selectedYear,
chosenEntryStatus: selectedReport
};
reportsDashboardResource.generateEntriesReportDownloadLink(reportDetail).then(function (response) {
if (response.Successful) {
$scope.hasCompleted = true;
} else {
notificationsService.error("Ooops", response.ErrorMessage);
}
$scope.actionInProgress = false;
}, generalError);
};
$scope.restart = function () {
$scope.init();
}
// Initialise Page
$scope.init();
}
angular.module("umbraco").controller("ReportsDashboardController", TreeReportsCHLController)
this is the code with the dropdowns in it;
<table>
<tr>
<td>Get a report for year: {{selectedYear}}</td>
<td><select ng-model="selectedYear" ng-change="yearHasSelected(selectedYear)" ng-options="year for year in entryYears" no-dirty-check></select></td>
</tr>
<tr ng-show="!hasCompleted && yearSelected">
<td>
Get Report Type:
</td>
<td>
<select ng-model="selectedReport" ng-change="generateFile(selectedReport)" ng-options="status for status in reportTypes" no-dirty-check ng-disabled="actionInProgress"></select>
</td>
</tr>
</table>
I've also done a further test where I simply set $scope.selectedYear to $scope.entryYears[0] within the reset method. When I console.log $scope.selectedYear here, the value confirms it has been changed, but strangely where I've outputted the $scope.selectedYear / {{selectedYear}} to the page for testing, this does not update. It's almost as though the binding between the controller and the view isn't occuring.
Any help?
Thank-you.
Here's a working plunk that is somewhat stripped down since I didn't have access to of the services that your are injecting into your controller. The changes I made in the controller are:
First,
$scope.entryYears = new Array();
becomes
$scope.entryYears = [];
as this is the preferred way to declare an array in js.
Second, I removed $scope.apply() that was wrapping
$scope.selectedYear = $scope.entryYears[0];
$scope.selectedReport = $scope.reportTypes[0];
as this was causing infinite digest cycles.
I've been trying to learn AngularJS recently, and hit a bump in the road with Localstorage i spend so many hours trying to make it save locally, I think that it's working as it should now, but now i would like to print out the data saved local from the JSON array, how can i go about that?
EDIT:
A bit of clarification, What im trying to achieve is getting the information i save in the localstorage out onto the website as a string, so it's readable. hope i'ts more understandable. Thanks in advance
My view.
<ion-list>
<div >
<ion-item ng-controller='ModalEditCtrl' ng-click="openModal()">
<div class="thumbnail" style="border:1px black solid">
</div>
<div ng-controller="createPerson" class="contactinfo" >
<li ng-repeat="contact in contactdetail.contactinfo"> {{contact.name}} </li>
</div>
</ion-item>
</div>
<div ng-controller="ModalAddCtrl">
<button type="button" ng-click="openModal()">+++</button>
</div>
</ion-list>
My controller
app.controller('createPerson', function ($scope) {
var id = id_counter = 1;
$scope.editorEnabled = false;
$scope.disableEditor = function() {
$scope.editorEnabled = false;
};
$scope.enableEditor = function() {
$scope.editorEnabled = true;
};
$scope.contactinfo = [
{name: 'test', phone: 1231, email: 'asd#asd.com'}
];
$scope.saveData = function () {
id_counter += 1;
$scope.editorEnabled = false;
$scope.contactinfo.push({
name: $scope.contactName,
phone: $scope.contactPhone,
email: $scope.contactEmail,
sort_id: id_counter
});
//$scope.todoText = ''; //clear the input after adding
localStorage.setItem('contactinfo', JSON.stringify($scope.contactinfo));
// localStorage.setItem("contacts", JSON.stringify(contacts));
}
$scope.loadData = function () {
var contacts = localStorage.getItem("contactinfo");
var contactdetail = JSON.parse(contacts); //
console.log(contactdetail);
}
$scope.clearData = function () {
window.localStorage.clear();
}
});
Your question is not very clear, I dont think you will be able to get much help unless you clean it up a little.
To print out the data (for debugging, usually) you could just add {{contactinfo|json}} somewhere in your html.
To actually display the data for use on the webpage the following should work for you.
<div ng-repeat="contact in contactinfo track by $index">
<div>Name: {{contact.name}}</div>
<div>Phone: {{contact.phone}}</div>
<div>Email: {{contact.email}}</div>
</div>
I think that some of that logic might be better split into a factory, too. Something like this maybe...?
var contactFactory = angular.module('contactFactory', []);
contactFactory.factory('contactInfo', ['$window', function ($window) {
var id = id_counter = 1;
var contacts = [];
function addContact(name, phone, email) {
id_counter += 1;
contacts.push({
name: name,
phone: phone,
email: email,
sort_id: id_counter
});
saveData();
}
function saveData(contactInfo) {
$window.localStorage.setItem('contactinfo', angular.fromJson(contacts));
}
function loadData() {
contacts = angular.toJson($window.localStorage.getItem('contactinfo'));
return contacts;
}
function clearData() {
$window.localStorage.removeItem('contactinfo');
}
return {
addContact: addContact,
saveData: saveData,
loadData: loadData,
clearData: clearData
};
}]);
var app = angular.module('yourAppName', ['contactFactory']);
app.controller('createPerson', ['$scope', 'contactInfo', function ($scope, contactInfo) {
$scope.editorEnabled = false;
$scope.disableEditor = function() {
$scope.editorEnabled = false;
};
$scope.enableEditor = function() {
$scope.editorEnabled = true;
};
$scope.contactinfo = [
{name: 'test', phone: 1231, email: 'asd#asd.com'}
];
$scope.saveData = function () {
contactInfo.addContact($scope.contactName, $scope.contactPhone, $scope.contactEmail);
$scope.editorEnabled = false;
}
$scope.loadData = contactInfo.loadData;
$scope.clearData = contactInfo.clearData;
}]);
Angular has wrapper for window, which should be used inside your code. There is also ngStorage module or many available solutions which are dealing with browser storage in Angular way. Moreover Angular has functions like angular.toJson() and angular.fromJson(). If e.g. jsonObj is JSON array then var obj = angular.fromJson(jsonObj) gives you JavaScript array. If jsonObj has array property inside then you should go with: var jsArray = angular.fromJson(jsonObj).array.
I am new to knockout. For my problem, I am trying to make it so that for each project, there is a button and textarea. The textarea will be hidden upon page load. If I click the button, it will show the textarea (toggle). Currently, if I click the button, ALL textareas on the page will show, rather than just the corresponding textarea.
I'm hoping the fix for this isn't too dramatic and involving a complete reworking of my code as by some magic, every other functionality has been working thus far. I added the {attr id: guid} (guid is a unique identifier of a project retrieved from the database) statement in an attempt to establish a unique ID so that the right controls were triggered...although that did not work.
Sorry I do not have a working jfiddle to show the issue... I tried to create one but it does not demonstrate the issue.
JS:
//if a cookie exists, extract the data and bind the page with cookie data
if (getCookie('filterCookie')) {
filterCookie = getCookie('filterCookie');
var cookieArray = filterCookie.split(",");
console.log(cookieArray);
$(function () {
var checkboxes = new Array();
for (var i = 0; i < cookieArray.length; i++) {
console.log(i + cookieArray[i]);
checkboxes.push(getCheckboxByValue(cookieArray[i]));
//checkboxes.push(document.querySelectorAll('input[value="' + cookieArray[i] + '"]'));
console.log(checkboxes);
checkboxes[i].checked = true;
}
})
filterCookie = getCookie('filterResultsCookie');
cookieArray = filterCookie.split(",");
filterCookieObj = {};
filterCookieObj.action = "updateProjects";
filterCookieObj.list = cookieArray;
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify(filterCookieObj)
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
//if the cookie doesn't exist, just bind the page
else {
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify({
action: "getProjects"
})
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
View Model:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
};
};
HTML:
<!-- ko foreach: projects -->
<div id="eachOppyProject" style="border-bottom: 1px solid #eee;">
<table>
<tbody>
<tr>
<td><a data-bind="attr: { href: '/tools/oppy/' + guid }" style="font-size: 25px;"><span class="link" data-bind=" value: guid, text: name"></span></a></td>
</tr>
<tr data-bind="text: projectDescription"></tr>
<%-- <tr data-bind="text: guid"></tr>--%>
</tbody>
</table>
<span class="forminputtitle">Have you done project this before?</span> <input type="button" value="Yes" data-bind="click: $parent.toggleTextArea" class="btnOppy"/>
<textarea placeholder="Tell us a little of what you've done." data-bind="visible: $parent.show, attr: {'id': guid }" class="form-control newSessionAnalyst" style="height:75px; " /><br />
<span> <input type="checkbox" name="oppyDoProjectAgain" style="padding-top:10px; padding-right:20px;">I'm thinking about doing this again. </span>
<br />
</div><br />
<!-- /ko -->
Spencer:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.projects().forEach(function() { //also tried proj.forEach(function())
self.projects().showComments = ko.observable(false);
self.projects().toggleComments = function () {
self.showComments(!self.showComments());
};
})
};
It's weird that
data-bind="visible: show"
doesn't provide any binding error because context of binding inside ko foreach: project is project not the ProjectViewModel.
Anyway, this solution should solve your problem:
function ViewModel() {
var self = this;
var wrappedProjects = proj.map(function(p) {
return new Project(p);
});
self.projects = ko.observableArray(wrappedProjects);
}
function Project(proj) {
var self = proj;
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
}
return self;
}
The problem is that the show observable needs to be defined in the projects array. Currently all the textareas are looking at the same observable. This means you'll have to move the function showTextArea into the projects array as well.
Also you may want to consider renaming your function or getting rid of it entirely. Function names which imply they drive a change directly to the view fly in the face of the MVVM pattern. I'd recommend a name like "toggleComments" as it doesn't reference a view control.
EDIT:
As an example:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
foreach(var project in self.projects()) {
project.showComments = ko.observable(false);
project.toggleComments = function () {
self.showComments(!self.showComments());
};
}
};
There is probably a much cleaner way to implement this in your project I just wanted to demonstrate my meaning without making a ton of changes to the code you provided.
I have an object that is constructed upon a table row from the database. It has all the properties that are found in that entry plus several ko.computed that are the middle layer between the entry fields and what is displayed. I need them to be able translate foreign keys for some field values.
The problem is the following: One of the properties is an ID for a string. I retrieve that ID with the computed. Now in the computed will have a value that looks like this: 'option1|option2|option3|option4'
I want the user to be able to change the options, add new ones or swap them around, but I also need to monitor what the user is doing(at least when he adds, removes or moves one property around). Hence, I have created an observable array that I will bind in a way that would allow me to monitor user's actions. Then the array will subscribe to the computed so it would update the value in the database as well.
Some of the code:
function Control(field) {
var self = this;
self.entry = field; // database entry
self.choices = ko.observableArray();
self.ctrlType = ko.computed({
read: function () {
...
},
write: function (value) {
if (value) {
...
}
},
owner: self
});
self.resolvedPropriety = ko.computed({
read: function () {
if (self.ctrlType()) {
var options = str.split('|');
self.choices(createObservablesFromArrayElements(options));
return str;
}
else {
return '';
}
},
write: function (value) {
if (value === '') {
//delete entry
}
else {
//modify entry
}
},
deferEvaluation: true,
owner: self
});
self.choices.subscribe(function (newValue) {
if (newValue.length !== 0) {
var newStr = '';
$.each(newValue, function (id, el) {
newStr += el.name() + '|';
});
newStr = newStr.substring(0, newStr.lastIndexOf("|"));
if (self.resolvedPropriety.peek() !== newStr) {
self.resolvedPropriety(newStr);
}
}
});
self.addChoice = function () {
//user added an option
self.choices.push({ name: ko.observable('new choice') });
};
self.removeChoice = function (data) {
//user removed an option
if (data) {
self.choices.remove(data);
}
};
...
}
This combination works, but not as I want to. It is a cyclic behavior and it triggers too many times. This is giving some overload on the user's actions because there are a lot of requests to the database.
What am I missing? Or is there a better way of doing it?
Quote from knockout computed observable documentation
... it doesn’t make sense to include cycles in your dependency chains.
The basic functionality I interpreted from the post:
Based on a field selection, display a list of properties/options
Have the ability to edit said property/option
Have the ability to add property/option
Have the ability to delete property/option
Have the ability to sort properties/options (its there, you have to click on the end/edge of the text field)
Have the ability to save changes
As such, I have provided a skeleton example of the functionality, except the last one, you described #JSfiddle The ability to apply the changes to the database can be addressed in several ways; None of which, unless you are willing to sacrifice the connection overhead, should include a computed or subscription on any changing data. By formatting the data (all of which I assumed could be collected in one service call) into a nice nested observable view model and passing the appropriate observables around, you can exclude the need for any ko.computed.
JS:
var viewModel = {
availableFields : ko.observableArray([
ko.observable({fieldId: 'Field1',
properties: ko.observableArray([{propertyName: "Property 1.1"}])}),
ko.observable({fieldId: 'Field2',
properties: ko.observableArray([{propertyName:"Property 2.1"},
{propertyName:"Property 2.2"}])})]),
selectedField: ko.observable(),
addProperty: function() {
var propertyCount = this.selectedField().properties().length;
this.selectedField().properties.push({propertyName: "Property " + propertyCount})
},
};
ko.applyBindings(viewModel);
$("#field-properties-list").sortable({
update: function (event, ui) {
//jquery sort doesnt affect underlying array so we have to do it manually
var children = ui.item.parent().children();
var propertiesOrderChanges = [];
for (var i = 0; i < children.length; ++i) {
var child = children[i];
var item = ko.dataFor(child);
propertiesOrderChanges.push(item)
}
viewModel.selectedField().properties(propertiesOrderChanges);
}
});
HTML:
<span>Select a field</span>
<select data-bind='foreach: availableFields, value: selectedField'>
<option data-bind='text: $data.fieldId, value: $data'></option>
</select>
<div style="padding: 10px">
<label data-bind='text: "Properties for " + selectedField().fieldId'></label>
<button data-bind='click: $root.addProperty'>Add</button>
<ul id='field-properties-list' data-bind='foreach: selectedField().properties'>
<li style = "list-style: none;">
<button data-bind="click: function() { $root.selectedField().properties.remove($data) }">Delete</button>
<input data-bind="value: $data.propertyName"></input>
</li>
</ul>
</div>