When extending $firebaseArray I get the error 'invalid key $id (cannot contain .$[]#)'
The issue is with the $id property on the book object. When calling $save on the array and passing the modified book in, the factory doesn't recognize $id as the id field like it normally would prior to extending.
Html
<ul>
<li ng-repeat="book in bookList"> {{book.$id}}: {{book.date|date:"medium"}}
<button ng-click="setDate(book)">set date</button>
</li>
</ul>
<pre>{{book|json}}</pre>
Javascript
// reference to our Firebase instance
var fb = new Firebase('YourUrl');
function Book(snap) {
// records in synchronized arrays must have a $id property
this.$id = snap.key();
this.update(snap);
}
Book.prototype.update = function (snap) {
angular.extend(this, snap.val());
// convert json dates to Date objects
this.date = new Date(this.date||0);
this.$priority = snap.getPriority();
};
Book.prototype.toJSON = function() {
return angular.extend({}, this, {
// revert Date objects to json data
date: this.date.getTime()
});
};
var app = angular.module('app', ['firebase']);
app.controller('ctrl', function ($scope, bookFactory) {
// create a synchronized array with a customized version
// of $FirebaseArray
$scope.bookList = new bookFactory(fb);
// loads a single record for display
$scope.book = "Click on a book id";
$scope.loadBook = function (book) {
$scope.book = book;
};
// changes the date on a book record, note that we're working
// with Date objects here
$scope.setDate = function(book) {
book.date = new Date();
$scope.bookList.$save(book);
};
});
app.factory('bookFactory', function ($firebaseArray, $firebaseUtils) {
return $firebaseArray.$extend({
// override $$added to return a Book object
$$added: function (snap, prevChild) {
return new Book(snap);
},
// override $$updated to update the book object
$$updated: function (snap) {
var rec = this.$getRecord(snap.name());
if(angular.isObject(rec) ) {
book.update(snap);
}
}
});
});
Made a fiddle showing the error, just press 'Set Date' on any of the books.
https://jsfiddle.net/jensenben/h98mjsoz/
This can be remedied if make the book object use id instead of $id and add a $$getKey method to the $extend call to tell it what field has the id in it, but I'd prefer to stick with the traditional $id field that I use when not extending a $firebaseArray.
function Book(snap) {
// records in synchronized arrays must have a $id property
this.id = snap.key();
this.update(snap);
}
return $firebaseArray.$extend({
$$getKey: function(rec) {
return rec.id;
},
...
There's a bit much going on, so I'm not sure if this is your problem. But to get rid of the error, convert the $firebaseObject() to a regular JSON object when saving:
$scope.setDate = function(book) {
book.date = new Date();
$scope.bookList.$save(book.toJSON());
};
Note that the CSS and most of the JavaScript code are irrelevant to the problem.
Related
I have an array of integers called data which I would like to send from my View to a specific controller, I could see that i can send integers and strings and it works with the code that I have so far, but when I try to send an array I can get the data correctly.
This is the code that I have in my view, it is something simple just to be in perspective.
function SeeStation() {
var data = [];
var i = 0;
$("input:checkbox:checked").each(function () {
data[i] = $(this).val();
});
window.location.href = "#Url.Action("ExportData", "Dispatch")?id=" + data;
}
and this is the code in the controller. I know it doesn't make much sense but so far I am focused on correctly obtaining the array by parameter.
public ActionResult ExportData(int[] id)
{
var data = cn.ESTACIONDESPACHOes.ToList();
return View(data);
}
In my array data I store something like this [1,2,3] and I would like to get something similar in the controller array id.
It will not bind like that.
To get the id array in your action you need to have the link at the end like this: *Dispatch/ExportData?id=1&id=2&id=3*
Your "#Url.Action("ExportData", "Dispatch")?id=" + data; will not generate that (data will give the numbers separated with commas).
You can just build the query string when you enumerate the checkboxes.
function SeeStation() {
var data = '';
$("input:checkbox:checked").each(function () {
data += 'id='$(this).val() + '&';
});
window.location.href = "#Url.Action("ExportData", "Dispatch")?" + data;
}
You will have a "&" in the end. You can easily remove it, but it will not affect anything.
There may be better ways to do this though, but I just used your function.
try
#Url.Action("ExportData", "Dispatch", new { id= [1,2,3] })
Store the Values in the Hidden Fields
#Html.HiddenFor(m => m.Ids, new { #Value = [1,2,3] })
Then Using the Ajax Get Method Pass the Hidden fields
In the Controller Method Convert the sting to array using string extension method
function SeeStation() {
var data = [];
var i = 0;
$("input:checkbox:checked").each(function () {
data[i] = $(this).val();
});
location.href = '#Url.Action("ExportData", "Dispatch")?id=' + data;
}
Please remove window keyword.
As I would like to create documents by merging the entries in a list into a Google Docs template. I have therefore integrated the DocumentMerge method from my previous question into a printButton in a list widget.
Clicking on the printButton should produce a document that merges the contents of the current row into the document template. But when I click on the printButton the method fails due to a circular reference. How can I fix that? The print method goes like this ...
function printReview(widget) {
var review = app.models.Review.getRecord(widget.datasource.item._key);
var templateId = 'templateId';
var filename = 'Review for ...' + new Date();
var copyFile = DriveApp.getFileById(templateId).makeCopy(filename);
var copyDoc = DocumentApp.openById(copyFile.getId());
var copyBody = copyDoc.getBody();
var fields = app.metadata.models.Review.fields;
for (var i in fields) {
var text = '$$' + fields[i].name + '$$';
var data = review[fields[i].name];
copyBody.replaceText(text, data);
}
copyDoc.saveAndClose();
}
As Morfinismo noticed you are getting the error because you are trying to pass complex object from client to server and serializer fails to handle it. In order to fix that you need to adjust your code:
// onClick button's event handler (client script)
function onPrintClick(button) {
var reviewKey = button.datasource.item._key;
google.script.run
.withSuccessHandler(function() { /* TODO */ })
.withFailureHandler(function() { /* TODO */ })
.printReview(reviewKey);
}
// server script
function printReview(reviewKey) {
var review = app.models.Review.getRecord(reviewKey);
...
}
I'm searching for a way to create a custom action button which allows me to make a new DataObject with pre-filled content from another DataObject. As a simple example: When I have an email and click the "answer"-button in my email-client, I get a new window with pre-filled content from the email before. I need exactly this functionality for my button. This button should appear next to each DataObject in the GridField.
So I know how to make a button and add it to my GridField (--> https://docs.silverstripe.org/en/3.2/developer_guides/forms/how_tos/create_a_gridfield_actionprovider/) and I know how to go to a new DataObject:
Controller::curr()->redirect($gridField->Link('item/new'));
I also found out that there is a duplicate function for DataObjects:
public function duplicate($doWrite = true) {
$className = $this->class;
$clone = new $className( $this->toMap(), false, $this->model );
$clone->ID = 0;
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
if($doWrite) {
$clone->write();
$this->duplicateManyManyRelations($this, $clone);
}
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
return $clone;
}
Perhaps it's easier than I think but at the moment I just don't get how to rewrite this to get what I need. Can somebody give me a hint?
That's for sure not the cleanest solution but I think it should do the trick.
At first let's create the custom gridfield action. Here we will save all accessible records in a session and add a query string to the url so that we'll know which object we want to "clone"
public function getColumnContent($gridField, $record, $columnName) {
if(!$record->canEdit()) return;
$field = GridField_FormAction::create(
$gridField,
'clone'.$record->ID,
'Clone',
'clone',
array('RecordID' => $record->ID)
);
$values = Session::get('ClonedData');
$data = $record->data()->toMap();
if($arr = $values) {
$arr[$record->ID] = $data;
} else {
$arr = array(
$record->ID => $data
);
}
Session::set('ClonedData', $arr);
return $field->Field();
}
public function getActions($gridField) {
return array('clone');
}
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
if($actionName == 'clone') {
$id = $arguments['RecordID'];
Controller::curr()->redirect($gridField->Link("item/new/?cloneID=$id"));
}
}
after adding this new component to our gridfield,
$gridField->getConfig()->addComponent(new GridFieldCustomAction());
we'll need to bring the data into the new form. To do so, add this code directly above "return $fields" on your getCMSFields function so it will be executed every time we'll open this kind of object.
$values = Session::get('ClonedData');
if($values) {
Session::clear('ClonedData');
$json = json_encode($values);
$fields->push(LiteralField::create('ClonedData', "<div id='cloned-data' style='display:none;'>$json</div>"));
}
At the end we need to bring the content back into the fields. We'll do that with a little bit of javascript so at first you need to create a new script.js file and include it in the ss backend (or just use an existing one).
(function($) {
$('#cloned-data').entwine({
onmatch: function() {
var data = JSON.parse($(this).text()),
id = getParameterByName('cloneID');
if(id && data) {
var obj = data[id];
if(obj) {
$.each(obj, function(i, val) {
$('[name=' + i + ']').val(val);
});
}
}
}
});
// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript#answer-901144
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
})(jQuery);
And that's it ... quite tricky. Hope it will solve your problem.
Scenario: I have a standard dropdown list and when the value in that dropdownlist changes I want to update another dropdownlist that exists in a tinyMCE control.
Currently it does what I want when I open the page (i.e. the first time)...
function changeParent() {
}
tinymce.create('tinymce.plugins.MoePlugin', {
createControl: function(n, cm) {
switch (n) {
case 'mylistbox':
var mlb = cm.createListBox('mylistbox', {
title: 'Inserts',
onselect: function(v) {
tinyMCE.execCommand("mceInsertContent",false,v);
}
});
<% foreach (var insert in (ViewData["Inserts"] as List<String>)) { %> // This is .NET
yourobject = '<%= insert %>'; // This is JS AND .NET
mlb.add(yourobject, yourobject); // This is JavaScript
<% } %>
// Return the new listbox instance
return mlb;
}
return null;
}
});
<%= Html.DropDownList(Model.Record[184].ModelEntity.ModelEntityId.ToString(), ViewData["Containers"] as SelectList, new { onchange = "changeParent(); return false;" })%>
I am thinking the way to accomplish this (in the ChangeParentFunction) is to call a controller action to get a new list, then grab the 'mylistbox' object and reassign it, but am unsure how to put it all together.
As far as updating the TinyMCE listbox goes, you can try using a tinymce.ui.NativeListBox instead of the standard tinymce.ui.ListBox. You can do this by setting the last argument to cm.createListBox to tinymce.ui.NativeListBox. This way, you'll have a regular old <select> that you can update as you normally would.
The downside is that it looks like you'll need to manually hook up your own onchange listener since NativeListBox maintains its own list of items internally.
EDIT:
I played around a bit with this last night and here's what I've come up with.
First, here's how to use a native list box and wire up our own onChange handler, the TinyMCE way:
// Create a NativeListBox so we can easily modify the contents of the list.
var mlb = cm.createListBox('mylistbox', {
title: 'Inserts'
}, tinymce.ui.NativeListBox);
// Set our own change handler.
mlb.onPostRender.add(function(t) {
tinymce.dom.Event.add(t.id, 'change', function(e) {
var v = e.target.options[e.target.selectedIndex].value;
tinyMCE.activeEditor.execCommand("mceInsertContent", false, v);
e.target.selectedIndex = 0;
});
});
As far as updating the list box at runtime, your idea of calling a controller action to get the new items is sound; I'm not familiar with ASP.NET, so I can't really help you there.
The ID of the <select> that TinyMCE creates takes the form editorId_controlId, where in your case controlId is "mylistbox". Firebug in Firefox is the easiest way to find the ID of the <select> :)
Here's the test button I added to my page to check if the above code was working:
<script type="text/javascript">
function doFoo() {
// Change "myEditor" below to the ID of your TinyMCE instance.
var insertsElem = document.getElementById("myEditor_mylistbox");
insertsElem.options.length = 1; // Remove all but the first option.
var optElem = document.createElement("option");
optElem.value = "1";
optElem.text = "Foo";
insertsElem.add(optElem, null);
optElem = document.createElement("option");
optElem.value = "2";
optElem.text = "Bar";
insertsElem.add(optElem, null);
}
</script>
<button onclick="doFoo();">FOO</button>
Hope this helps, or at least gets you started.
Step 1 - Provide a JsonResult in your controller
public JsonResult GetInserts(int containerId)
{
//some code to get list of inserts here
List<string> somedata = doSomeStuff();
return Json(somedata);
}
Step 2 - Create javascript function to get Json results
function getInserts() {
var params = {};
params.containerId = $("#184").val();
$.getJSON("GetInserts", params, updateInserts);
};
updateInserts = function(data) {
var insertsElem = document.getElementById("183_mylistbox");
insertsElem.options.length = 1; // Remove all but the first option.
var optElem = document.createElement("option");
for (var item in data) {
optElem = document.createElement("option");
optElem.value = item;
optElem.text = data[item];
try {
insertsElem.add(optElem, null); // standards compliant browsers
}
catch(ex) {
insertsElem.add(optElem, item+1); // IE only (second paramater is the items position in the list)
}
}
};
Step 3 - Create NativeListBox (code above provided by ZoogieZork above)
var mlb = cm.createListBox('mylistbox', {
title: 'Inserts'
}, tinymce.ui.NativeListBox);
// Set our own change handler.
mlb.onPostRender.add(function(t) {
tinymce.dom.Event.add(t.id, 'change', function(e) {
var v = e.target.options[e.target.selectedIndex].value;
tinyMCE.activeEditor.execCommand("mceInsertContent", false, v);
e.target.selectedIndex = 0;
});
});
//populate inserts on listbox create
getInserts();
I need to return the value from my Responder object. Right now, I have:
private function pro():int {
gateway.connect('http://10.0.2.2:5000/gateway');
var id:int = 0;
function ret_pr(result:*):int {
return result
}
var responder:Responder = new Responder(ret_pr);
gateway.call('sx.xj', responder);
return id
}
Basically, I need to know how to get the return value of ret_pr into id or anything that I return from that function. The responder just seems to eat it. I can't use a public variable somewhere else because this will be running multiple times at once, so I need local scope.
This is how I'd write a connection to the AMF server, call it and store the resulting value. Remember that the result won't be available instantly so you'll set up the responder to "respond" to the data once it returns from the server.
public function init():void
{
connection = new NetConnection();
connection.connect('http://10.0.2.2:5000/gateway');
setSessionID( 1 );
}
public function setSessionID(user_id:String):void
{
var amfResponder:Responder = new Responder(setSessionIDResult, onFault);
connection.call("ServerService.setSessionID", amfResponder , user_id);
}
private function setSessionIDResult(result:Object):void {
id = result.id;
// here you'd do something to notify that the data has been downloaded. I'll usually
// use a custom Event class that just notifies that the data is ready,but I'd store
// it here in the class with the AMF call to keep all my data in one place.
}
private function onFault(fault:Object):void {
trace("AMFPHP error: "+fault);
}
I hope that can point you in the right direction.
private function pro():int {
gateway.connect('http://10.0.2.2:5000/gateway');
var id:int = 0;
function ret_pr(result:*):int {
return result
}
var responder:Responder = new Responder(ret_pr);
gateway.call('sx.xj', responder);
return id
}
This code is never going to get you what you want. You need to use a proper result function. The anonymous function responder return value will not be used by the surrounding function. It will always return 0 in this case. You are dealing with an asynchronous call here, and your logic needs to handle that accordingly.
private function pro():void {
gateway.connect('http://10.0.2.2:5000/gateway');
var responder:Responder = new Responder(handleResponse);
gateway.call('sx.xj', responder);
}
private function handleResponse(result:*):void
{
var event:MyCustomNotificationEvent = new MyCustomNotificationEvent(
MyCustomNotificationEvent.RESULTS_RECEIVED, result);
dispatchEvent(event);
//a listener responds to this and does work on your result
//or maybe here you add the result to an array, or some other
//mechanism
}
The point there being using anon functions/closures isn't going to give you some sort of pseudo-syncronous behavior.