an array of ArrayControllers - javascript

I'm doing something wrong here, but can't figure out what. I'm new to both Ember and Javascript in general, so feel free to point out any mistakes. I would appreciate an additional pair of eyes.
I basically have a google map with multiple datasets. In the controller that goes with the view I get the datasets and create an dataSetController(ArrayController) for each dataset. I then let the dataSetController load the data and add it to it's content, and an additional marker array.
When the process is done however, both dataSetControllers contain all points, instead of just the points for the particular dataset.
Below is the controller that goes with the view:
App.MapviewShowController = Ember.ObjectController.extend({
dataSets: [],
createDataSets: function() {
'use strict';
var self = this;
// clean previous data
this.get('dataSets').length = 0;
$.ajax({
url: '/active_data_sets.json',
type: 'GET',
data: {'project_id': this.get('id')},
success: function(data) {
data.active_data_sets.forEach(function(entry) {
// create a new controller for this dataset
var newds = App.AddressRecordController.create();
self.get('dataSets').pushObject(newds);
});
},
error: function() {
}
});
}
});
And the dataSetController itself:
App.AddressRecordController = Ember.ArrayController.extend({
content: [],
isActive: true,
dataSetId: 0,
markerColor: '',
datasetName: '',
map: null,
map_nelat: null,
map_nelng: null,
map_swlat: null,
map_swlng: null,
markerIcon: null,
markers: [],
mapBinding: 'App.MapData.map',
map_nelatBinding: 'App.MapData.ne_lat',
map_nelngBinding: 'App.MapData.ne_lng',
map_swlatBinding: 'App.MapData.sw_lat',
map_swlngBinding: 'App.MapData.sw_lng',
getAddresses: function(ne_lat, ne_lng, sw_lat, sw_lng) {
"use strict";
var self = this;
$.ajax({
url: '/address_records.json',
type: 'GET',
data: {'dataset_id': this.get('dataSetId'), 'ne_lat': ne_lat, 'ne_lng': ne_lng, 'sw_lat': sw_lat, 'sw_lng': sw_lng},
success: function(data) {
data.address_records.forEach(function(new_address) {
if (!self.findProperty('id', new_address.id)) {
// add to the content
self.content.addObject(App.AddressRecord.create(new_address));
// add the marker
var marker = new google.maps.Marker({
position: new google.maps.LatLng(new_address.lat, new_address.long),
map: self.get('map'),
animation: google.maps.Animation.DROP,
title: 'marker',
id: new_address.id
});
// add the marker for later reference
self.markers.push(marker);
}
});
},
error: function() {
}
});
},
newBounds: function() {
"use strict";
this.getAddresses(this.map_nelat, this.map_nelng, this.map_swlat, this.map_swlng);
}.observes('map_swlng'),
clean: function() {
'use strict';
// clean the objects in arracycontroller
this.forEach(function(el) {
el.destroy();
});
// clean the markers
this.markers.length = 0;
},
showMarkers: function() {
'use strict';
var self = this;
if(this.get('isActive')) {
this.markers.forEach(function(mkr) {
mkr.setMap(self.get('map'));
});
} else {
this.markers.forEach(function(mkr) {
mkr.setMap(null);
});
}
}.observes('isActive')
});
Update
AFter further debugging I found out that multiple AddressRecordControllers share nothing except the markers array. To circumvent the issue I now store the markers as content and that works fine. Still not clear about why the markers array is shared over different controllers.

I believe the create methods is more like a singleton so it either creates the object or returns a pointer to the object. So you are just adding to the same controller. you might try instead. Ember also has a Mixin thing that I am not sure how it works yet either.
var newds = App.AddressRecordController.extend();

Related

Using google map in Vue / Laravel

Trying to implement google map into Vue component. But having a hard time. Actually, there is no error. But no map also :) Okay, what I tried so far down below.
In laravel blade I set my api.
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{env('GOOGLE_MAPS_API')}}&callback=initMap"></script>
Then in Vue component;
data() {
return {
mapName: "map",
//some other codes
}
},
mounted() {
this.fetchEstates();
},
methods: {
fetchEstates(page = 1) {
axios.get('/ajax', {
params: {
page
}}).then((response) => {
// console.log(response);
this.estates = response.data.data;
//some other codes....
//some other codes....
},
computed: {
//some other functions in computed...
//
initMap: function(){
var options =
{
zoom : 6,
center : {
lat:34.652500,
lng:135.506302
}
};
var map = new google.maps.Map(document.getElementById(this.mapName), options);
var marker = new google.maps.Marker({
map: map,
icon: 'imgs/marker.png',
url: "/pages/estates.id",
label: {
text: this.estates.price,
color: "#fff",
},
position: {
lat: this.estates.lat,
lng: this.estates.lng
}
});
google.maps.event.addListener(marker, 'click', function () {
window.location.href = this.url;
});
}
<div id="map"></div>
and last marker url id bind is in controller like this,
public function details($id)
{
$estates = allestates::where('id', $id)->first();
return view('pages.details', compact('estates'));
}
Do I missing something in Vue js? Thank you!
From our discussion in the comments, I realise that your issue is because this.estates is still not defined when initMap() is executed. Remember that you are using an asynchronous operation (via axios) to populate this.estates, so it is undefined at runtime. What you can do is:
Keep the map initialisation logic in initMap()
Move all the Google Map marker creation until after the axios promise has been resolved. You can abstract all that into another method, e.g. insertMarkers()
Also, remember that you need to define estates in the app/component data, otherwise it will not be reactive.
Here is an example:
data() {
return {
mapName: "map",
// Create the estate object first, otherwise it will not be reactive
estates: {}
}
},
mounted() {
this.fetchEstates();
this.initMap();
},
methods: {
fetchEstates: function(page = 1) {
axios.get('/ajax', {
params: {
page
}}).then((response) => {
this.estates = response.data.data;
// Once estates have been populated, we can insert markers
this.insertMarkers();
//pagination and stuff...
});
},
// Iniitialize map without creating markers
initMap: function(){
var mapOptions =
{
zoom : 6,
center : {
lat:34.652500,
lng:135.506302
}
};
var map = new google.maps.Map(document.getElementById(this.mapName), mapOptions);
},
// Helper method to insert markers
insertMarkers: function() {
var marker = new google.maps.Marker({
map: map,
icon: 'imgs/marker.png',
url: "/pages/estates.id",
label: {
text: this.estates.price,
color: "#fff",
},
position: {
lat: this.estates.lat,
lng: this.estates.lng
}
});
google.maps.event.addListener(marker, 'click', function () {
window.location.href = this.url;
});
}
},
Update: It also turns out that you have not addressed the issue of the data structure of this.estates. It appears that you are receiving an array from your endpoint instead of objects, so this.estates will return an array, and of course this.estates.lat will be undefined.
If you want to iterate through the entire array, you will have to use this.estates.forEach() to go through each individual estates while adding the marker, i.e.:
data() {
return {
mapName: "map",
// Create the estate object first, otherwise it will not be reactive
estates: {}
}
},
mounted() {
this.fetchEstates();
this.initMap();
},
methods: {
fetchEstates: function(page = 1) {
axios.get('/ajax', {
params: {
page
}}).then((response) => {
this.estates = response.data.data;
// Once estates have been populated, we can insert markers
this.insertMarkers();
//pagination and stuff...
});
},
// Iniitialize map without creating markers
initMap: function(){
var mapOptions =
{
zoom : 6,
center : {
lat:34.652500,
lng:135.506302
}
};
var map = new google.maps.Map(document.getElementById(this.mapName), mapOptions);
},
// Helper method to insert markers
insertMarkers: function() {
// Iterate through each individual estate
// Each estate will create a new marker
this.estates.forEach(estate => {
var marker = new google.maps.Marker({
map: map,
icon: 'imgs/marker.png',
url: "/pages/estates.id",
label: {
text: estate.price,
color: "#fff",
},
position: {
lat: estate.lat,
lng: estate.lng
}
});
google.maps.event.addListener(marker, 'click', function () {
window.location.href = this.url;
});
});
}
},
From what I can see in the screenshot you posted, this.estates is an array of objects? If that's the case you need to iterate through the array using forEach
this.estates.forEach((estate, index) => {
console.log(estate.lat);
//handle each estate object here
});
or use the first item in the array like so this.estates[0].lat, if you're only interested in the first item.

Bind knockoutjs to javascript object property

I'm new to Knockoutjs, so please bear with me.
I want to knocoutjs bind a DxForm (DevExpress) to an javascript object property, but I get an error ... "Cannot read property 'items' of undefined".
I am uncertain if this is a knockout problem, DevExpress problem or just incufficient coding skills from my part.
Here's my code...
HTML:
<div data-bind="dxForm: frm.options"></div>
Javascript:
var viewModel = function() {
var that = this;
// -----------------------------------------------------------------
// Faste...
// -----------------------------------------------------------------
that.frm = {
items: ko.observable(undefined),
data: ko.observable(undefined),
instance: ko.observable({}),
options: {
items: that.frm.items,
formData: that.frm.data,
onInitialized: function(e) {
that.frm.instance(e.component);
},
},
};
return {
frm: that.frm,
};
};
var vm = new viewModel();
ko.applyBindings(vm);
var items = [{
"dataField": "test",
"editorOptions": {
"type": "date",
"pickerType": "calendar",
},
"editorType": "dxDateBox",
"name": "Test",
"visible": true
}];
var data = {
test: 10,
};
vm.frm.data(data);
vm.frm.items(items);
JSFiddle: https://jsfiddle.net/MojoDK/ke395v2c/3/
I want to bind to objects since I'm going to use several DxForm objects and I like to keep the code to each DxForm in an object (easier to read).
Any idea why it fails?
Thanks.
You just have a problem with closure in your frm.
The that property in frm object do not exist you should use this...
But in your onInitialized function, this and that will not target your viewModel object...
So this way, the easiest is to declare options object later :
that.frm = {
items: ko.observable(undefined),
data: ko.observable(undefined),
instance: ko.observable({})
};
that.frm.options = {
items: that.frm.items,
formData: that.frm.data,
onInitialized: function(e) {
that.frm.instance(e.component);
},
};
Updated jsfiddle

Bind OracleJet ojtimeline component to viewModel

I am trying to understand how I can bind data from the view-model to the view. The REST request to the back-end is working fine and I get a JSON array with several items. The existing documentation doesn't give me enough help.
How can I bind the timeline component ojtimeline to the view-model data array?
Edit: No errors now, since the view recognize the view-model array. But the ojtimeline doesn't display the data, only a working empty view component.
View
<div id="tline"
data-bind='ojComponent: {
component: "ojTimeline",
minorAxis: {
scale: "hours",
zoomOrder: ["hours", "days", "weeks"]
},
majorAxis: {
scale: "weeks"
},
start: new Date("Jan 1, 2016").toISOString(),
end: new Date("Jun 31, 2016").toISOString(),
referenceObjects: [{value: new Date("Feb 1, 2010").toISOString()}],
series: [{
id: "id",
emptyText: "No Data.",
items: statusArray,
label: "Oracle Events"
}],
overview: {
rendered: "off"
}
}' style="width: '100%';height: 350px"></div>
View-model
define(['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojknockout', 'ojs/ojtimeline'],
function (oj, ko) {
/**
* The view model for the main content view template
*/
function timelineContentViewModel() {
var self = this;
this.statusArray = ko.observableArray([]);
self.addData = function () {
$.ajax({
url: "http://localhost:8080/myproject/rest/status/v1/findAll",
type: 'GET',
dataType: 'json',
success: function (data, textStatus, jqXHR) {
var x = data;
for (i = 0; i < x.length; i++) {
statusArray.push({
id: data[i].id,
description: data[i].text,
title: data[i].user.screenName,
start: data[i].createdAt});
}
//$("#tline").ojTimeline("refresh"); Doesn't have ant affect
}
});
};
self.addData();
}
return timelineContentViewModel;
});
The ReferenceError is caused by
var statusArray = ko.observableArray([]);
it should be
this.statusArray = ko.observableArray([])
You will also (probably) need to refresh the timeline when the observable array has changed, e.g. after the for-loop in success callback:
...
success: function (data, textStatus, jqXHR) {
var x = data;
for (i = 0; i < x.length; i++) {
self.statusArray.push({
id: data[i].id,
description: data[i].text,
title: data[i].user.screenName,
start: data[i].createdAt});
}
$("#tline").ojTimeline("refresh");
}
...
I have loaded ojTimeline from Ajax data and have never needed to use refresh. Worst case, you can wrap the ojTimeline in a <!-- ko if ... --> so that the timeline doesn't appear until you have an Ajax response.
For the ojTimeline items attribute, instead of referencing the observable, I had to unwrap the observable like this: items: ko.toJS(statusArray).
Another thing to consider is pushing into an ko.observableArray inside a for loop. Each push using the ko.observableArray push() method invokes subscriptions. If your array is bound to the UI, then each push will trigger a DOM change. Instead, it is often better to push into the underlying array (unwrap the array) and then invoke self.statusArray.valueHasMutated. You may also want to keep an eye on your use of this, self, and nothing. Consistency will help avoid bugs like the one ladar identified.
What do you think about rewriting your for loop like this (code untested)?
ko.utils.arrayPushAll(
self.statusArray(),
ko.utils.arrayMap(data, function(item) {
return {
id: item.id,
description: item.text,
title: item.user.screenName,
start: item.createdAt;
};
});
);
self.statusArray.valueHasMutated();
Or, if you can get away with it (some OJ components don't like this approach), you can skip the push and just replace the entire array inside the observable:
self.statusArray(
ko.utils.arrayMap(data, function(item) {
return {
id: item.id,
description: item.text,
title: item.user.screenName,
start: item.createdAt;
};
});
);

Backbone.Paginator infinite mode, with Marionette

In my Marionette app, I have a Collection view, with a childView for it's models.
The collection assigned to the CollectionView is a PageableCollection from Backbone.paginator. The mode is set to infinite.
When requesting the next page like so getNextPage(), the collection is fetching data and assigning the response to the collection, overwriting the old entries, though the full version is store in collection.fullCollection. This is where I can find all entries that the CollectionView needs to render.
Marionette is being smart about collection events and will render a new childView with it's new model when a model is being added to the collection. It will also remove a childView when it's model was removed.
However, that's not quite what I want to do in this case since the collection doesn't represent my desired rendered list, collection.fullCollection is what I want to show on page.
Is there a way for my Marionette view to consider collection.fullCollection instead of collection, or is there a more appropriate pagination framework for Marionette?
Here's a fiddle with the code
For those who don't like fiddle:
App = Mn.Application.extend({});
// APP
App = new App({
start: function() {
App.routr = new App.Routr();
Backbone.history.start();
}
});
// REGION
App.Rm = new Mn.RegionManager({
regions: {
main: 'main',
buttonRegion: '.button-region'
}
});
// MODEL
App.Model = {};
App.Model.GeneralModel = Backbone.Model.extend({});
// COLLECTION
App.Collection = {};
App.Collection.All = Backbone.PageableCollection.extend({
model: App.Model.GeneralModel,
getOpts: function() {
return {
type: 'POST',
contentType: 'appplication/json',
dataType: 'json',
data: {skip: 12},
add: true
}
},
initialize: function() {
this.listenTo(Backbone.Events, 'load', (function() {
console.log('Load more entries');
// {remove: false} doesn't seem to affect the collection with Marionette
this.getNextPage();
})).bind(this)
},
mode: "infinite",
url: "https://api.github.com/repos/jashkenas/backbone/issues?state=closed",
state: {
pageSize: 5,
firstPage: 1
},
queryParams: {
page: null,
per_page: null,
totalPages: null,
totalRecords: null,
sortKey: null,
order: null
},
/*
// Enabling this will mean parseLinks don't run.
sync: function(method, model, options) {
console.log('sync');
options.contentType = 'application/json'
options.dataType = 'json'
Backbone.sync(method, model, options);
}
*/
});
// VIEWS
App.View = {};
App.View.MyItemView = Mn.ItemView.extend({
template: '#item-view'
});
App.View.Button = Mn.ItemView.extend({
template: '#button',
events: {
'click .btn': 'loadMore'
},
loadMore: function() {
Backbone.Events.trigger('load');
}
});
App.View.MyColView = Mn.CollectionView.extend({
initialize: function() {
this.listenTo(this.collection.fullCollection, "add", this.newContent);
this.collection.getFirstPage();
},
newContent: function(model, col, req) {
console.log('FullCollection length:', this.collection.fullCollection.length, 'Collection length', this.collection.length)
},
childView: App.View.MyItemView
});
// CONTROLLER
App.Ctrl = {
index: function() {
var col = new App.Collection.All();
var btn = new App.View.Button();
var colView = new App.View.MyColView({
collection: col
});
App.Rm.get('main').show(colView);
App.Rm.get('buttonRegion').show(btn);
}
};
// ROUTER
App.Routr = Mn.AppRouter.extend({
controller: App.Ctrl,
appRoutes: {
'*path': 'index'
}
});
App.start();
You could base the CollectionView off the full collection, and pass in the paged collection as a separate option:
App.View.MyColView = Mn.CollectionView.extend({
initialize: function(options) {
this.pagedCollection = options.pagedCollection;
this.pagedCollection.getFirstPage();
this.listenTo(this.collection, "add", this.newContent);
},
// ...
}
// Create the view
var colView = new App.View.MyColView({
collection: col.fullCollection,
pagedCollection: col
});
Forked fiddle

Backbonejs when to initialize collections

I'm building small one page application with rails 3.1 mongodb and backbonejs.
I have two resources available through json api. I created two models and collections in backbone which look like this
https://gist.github.com/1522131
also I have two seprate routers
projects router - https://gist.github.com/1522134
notes router - https://gist.github.com/1522137
I generated them with backbonejs-rails gem from github so code inside is just template. I initialize my basic router inside index.haml file
#projects
:javascript
$(function() {
window.router = new JsonApi.Routers.ProjectsRouter({projects: #{#projects.to_json.html_safe}});
new JsonApi.Routers.NotesRouter();
Backbone.history.start();
});
I don't want fetch notes when application is starting, because there is big chance that user will never look inside notes. So there isn't good reason to fetch it on start. Inside NotesRouter in all action I rely on #notes variable but without .fetch() method this variable is empty. Also I should can reproduce notes view from url like
/1/notes/5
project_id = 1
note_id = 5
What is best practices in backbonejs to solve this kind of problem ?
Why don't you lazy load the notes when it's requested? Here's an example:
var State = Backbone.Model.extend({
defaults: {
ready: false,
error: null
}
});
var Note = Backbone.Model.extend({
initialize: function () {
this.state = new State();
}
});
var Notes = Backbone.Collection.extend({
model: Note,
initialize: function () {
this.state = new State();
}
});
var NoteCache = Backbone.Model.extend({
initialize: function () {
this._loading = false;
this._loaded = false;
this._list = new Notes();
},
_createDeferred: function (id) {
var note = new Note({ id: id });
this._list.add(note);
this._load();
return note;
},
getNote: function (id) {
return this._list.get(id) || this._createDeferred(id);
},
getNotes: function () {
if (!this._loaded)
this._load();
return this._list;
},
_load: function () {
var that = this;
if (!this._loading) {
this._list.state.set({ ready: false, error: null });
this._loading = true;
$.ajax({
url: '/api/notes',
dataType: 'json',
cache: false,
type: 'GET',
success: function (response, textStatus, jqXHR) {
_.each(response.notes, function (note) {
var n = that._list.get(note.id);
if (n) {
n.set(note);
} else {
that._list.add(note, { silent: true });
n = that._list.get(note.id);
}
n.state.set({ ready: true, error: null });
});
that._list.state.set({ ready: true, error: null });
that._list.trigger('reset', that._list);
that._loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
that._list.state.set({ error: 'Error retrieving notes.' });
that._list.each(function (note) {
note.state.set({ error: 'Error retrieving note.' });
});
},
complete: function (jqXHR, textStatus) {
that._loading = false;
}
});
}
}
});
In this example, I'm defining a NoteCache object that manages the lazy loading. I also add a "state" property to the Note model and Notes collection.
You'll probably want to initialize NoteCache somewhere (probably inside your route) and whenever you want a note or notes, just do this:
var note = noteCache.getNote(5);
var notes = noteCache.getNotes();
Now inside your view, you'll want to listen for state changes in case the note/notes is not loaded yet:
var NoteView = Backbone.View.extend({
initialize: function(){
this.note.state.bind('change', this.render, this);
},
render: function(){
if (this.note.state.get('error') {
// todo: show error message
return this;
}
if (!this.note.state.get('ready') {
// todo: show loader animation
return this;
}
// todo: render view
return this;
}
});
I haven't tested this, so there may be some bugs, but I hope you get the idea.

Categories

Resources