How to get related data from "joined" Firebase collections - AngularFire [duplicate] - javascript

I am currently working on an app using firebase and angularJS (ionic). Basically this is a car management app, so you have people sharing their cars with others. I tried to structure the data as flat as possible to be efficient. My issue here is that if without problem I can display the list of the car_id of the different cars shared with the logged user, I can't find a way to display the list of cars shared with the user displaying the year and the model.
Thank you in advance for your help !
{
"rules": {
"users": {
".write": true,
"$uid": {
".read": "auth != null && auth.uid == $uid"
},
"cars": {
"car_id":true,
"role":true // Owner, borower...
}
},
"cars": {
"car_id":true,
"model":true,
"year":true
}
}
}
carapp.controller("carsController", function($scope, $firebaseObject, $ionicPopup, $ionicHistory) {
$ionicHistory.clearHistory();
$scope.list = function() {
frbAuth = frb.getAuth();
if(frbAuth) {
var userObject = $firebaseObject(frb.child("users/" + frbAuth.uid));
userObject.$bindTo($scope, "user");
$scope.cars = frb.child("cars");
}}
$scope.createCar = function() {
$ionicPopup.prompt({
model: 'Create a new car',
inputType: 'text'
})
.then(function(result) {
if(result !== "") {
var newCar = $scope.cars.push({
model: result
})
var newCarId = newCar.key();
$scope.user.cars.push({car_id: newCarId, role: "owner" });
} else {
console.log("Action not completed");
}
});
}
});
<div class="list">
<a ng-repeat="car in user.cars" >
<h2>{{car.car_id}}</h2> ----> works fine !
<h2>{{car.model}}</h2> ----> How to get this working ?
<h2>{{car.year}}</h2> ----> How to get this working ?
</a>
</div>

In the users/ path, begin by storing the list of cars by index, instead of in an array. So your structure would be:
{
"users": {
"kato": {
"cars": {
"DeLorean": true
}
}
},
"cars": {
"DeLorean": {
model: "DeLorean",
year: "1975"
}
}
}
To join this using AngularFire, you have several approaches available. An AngularFire-only solution might look like this, taking advantage of $extend:
app.factory('CarsByUser', function($firebaseArray) {
return $firebaseArray.$extend({
$$added: function(snap) {
return new Car(snap);
},
$$updated: function(snap) {
// nothing to do here; the value of the index is not used
},
$$removed: function(snap) {
this.$getRecord(snap.key()).destroy();
},
// these could be implemented in a manner consistent with the
// use case and above code, for simplicity, they are disabled here
$add: readOnly,
$save: readOnly
});
var carsRef = new Firebase(...).child('cars');
function Car(snap) {
// create a reference to the data for a specific car
this.$id = snap.key();
this.ref = carsRef.child(this.$id);
// listen for changes to the data
this.ref.on('value', this.updated, this);
}
Car.prototype.updated = function(snap) {
this.model = data.model;
this.year = data.year;
}
Car.prototype.destroy = function() {
this.ref.off('value', this.meta, this);
};
function readOnly() { throw new Error('This is a read only list'); }
});
app.controller('...', function($scope, CarsByUser, authData) {
// authenticate first, preferably with resolve
var ref = new Firebase(...).child(authData.uid);
$scope.cars = CarsByUser($scope);
});
For a more sophisticated and elegant approach, one could utilize NormalizedCollection and pass that ref into the AngularFire array:
app.controller('...', function($scope, $firebaseArray) {
var ref = new Firebase(...);
var nc = new Firebase.util.NormalizedCollection(
ref.child('users/' + authData.uid),
ref.child('cars')
)
.select('cars.model', 'cars.year')
.ref();
$scope.cars = $firebaseArray(nc);
});

Related

Set object in data from a method in VUE.js

I have been stuck with this issues for 2 hours now and I really can't seem to get it work.
const app = new Vue({
el: '#book-search',
data: {
searchInput: 'a',
books: {},
},
methods: {
foo: function () {
axios.get('https://www.googleapis.com/books/v1/volumes', {
params: {
q: this.searchInput
}
})
.then(function (response) {
var items = response.data.items
for (i = 0; i < items.length; i++) {
var item = items[i].volumeInfo;
Vue.set(this.books[i], 'title', item.title);
}
})
.catch(function (error) {
console.log(error);
});
}
}
});
When I initiate search and the API call I want the values to be passed to data so the final structure looks similar to the one below.
data: {
searchInput: '',
books: {
"0": {
title: "Book 1"
},
"1": {
title: "Book 2"
}
},
Currently I get Cannot read property '0' of undefined.
Problem lies here:
Vue.set(this.books[i], 'title', item.title);
You are inside the callback context and the value of this is not the Vue object as you might expect it to be. One way to solve this is to save the value of this beforehand and use it in the callback function.
Also instead of using Vue.set(), try updating the books object directly.
const app = new Vue({
el: '#book-search',
data: {
searchInput: 'a',
books: {},
},
methods: {
foo: function () {
var self = this;
//--^^^^^^^^^^^^ Save this
axios.get('https://www.googleapis.com/books/v1/volumes', {
params: {
q: self.searchInput
//-^^^^--- use self instead of this
}
})
.then(function (response) {
var items = response.data.items
var books = {};
for (i = 0; i < items.length; i++) {
var item = items[i].volumeInfo;
books[i] = { 'title' : item.title };
}
self.books = books;
})
.catch(function (error) {
console.log(error);
});
}
}
});
Or if you want to use Vue.set() then use this:
Vue.set(self.books, i, {
'title': item.title
});
Hope this helps.
yep, the problem is about context. "this" returns not what you expect it to return.
you can use
let self = this;
or you can use bind
function(){this.method}.bind(this);
the second method is better.
Also google something like "how to define context in js", "bind call apply js" - it will help you to understand what is going wrong.
// update component's data with some object's fields
// bad idea, use at your own risk
Object
.keys(patch)
.forEach(key => this.$data[key] = patch[key])

Ember display model which has relationship in one component

I want to expose my models to one component and show in one table. I want this to be modular in a way that I can use this component to other models.
The way I have done the attributes which have certain relationship do not show up. The problem that I found out is because by the time I grab it, the promise has not been resolved and I am not grabbing the attributes using {{ember-data}}. I cant figure out one way of doing that... I'm really new to ember and its' been a problem for me...
UPDATE
Before reading all the description below, the thing is that if I can convert the model to one array it would be enough, I guess. So, I could do something like this:
{#each model as |item|}}
<tr>
{{#each item as |column index|}}
<td>{{column.[index]}} </td>
{{/each}}
</tr>
{{/each}}
END OF UPDATE
I have the following two models
// #models/inventory-summary.js
export default DS.Model.extend({
name: DS.attr('string'),
total: DS.attr('number'),
projects: DS.hasMany('inventoryProject'), //I WANT TO SHOW THIS GUY HERE
});
// #models/inventory-project.js
export default DS.Model.extend({
name: DS.attr('string'), // IN PARTICULAR THIS ONE
summary: DS.hasMany('inventorySummary'),
});
Templates:
// #templates/inventory-summary.js
// here I'm sending the model to the component and mapping the attribute to something readable
{{model-table model=inventorySearchResult columnMap=columnMap}}
// #templates/components/model-table.hbs
// here is where I show the value of the model
{{#each model_table.values as |item|}}
{{getItemAt item index}}
{{/each}}
My helper
export function getItemAt(params/*, hash*/) {
return params[0][params[1]];
}
And in my route I'm doing:
// #routes/inventory-summary.js
model(params) {
let query_params = {page_size: 100};
if (params.page !== undefined && params.page !== null) {
query_params['page'] = params.page;
}
return Ember.RSVP.hash({
inventoryProject: this.get('store').findAll('inventoryProject'),
inventorySummary: this.get('store').query('inventorySummary', query_params),
});
},
setupController(controller, models) {
this._super(...arguments);
controller.set('projects', models.inventoryProject);
controller.set('inventorySearchResult', models.inventorySummary);
let columnMap = [
['name', 'Name ID',],
['total', 'Total'],
['projects', 'Project', {'property': 'name', 'has_many': true}]
];
controller.set('columnMap', columnMap);
},
Finally this is the part of the code which is really messed up which is where I'm passing to the template the values I'm trying to show
// #components/model-table.js
getValueForColumn(values, params) {
if (values.length > 0) {
if (typeof values[0] === "object") {
return this.getResolved(values, params);
} else {
return values;
}
}
return values;
},
getResolved(promise, params) {
let result = [];
promise.forEach( (data) => {
data.then((resolved) => {
let tmp = "";
resolved.forEach((item) => {
console.log(item.get(property)); // THIS GUY SHOWS UP LATER
tmp += item.get(property) + ", ";
});
result.push(tmp);
});
});
return result; // THIS GUY RETURN AS EMPTY!
},
didReceiveAttrs() {
this._super(...arguments);
let model = this.get('model');
let columnMap = this.get('columnMap');
for (var i = 0; i < columnMap.length; i++) {
attributes.push(columnMap[i][0]);
columns.push({'name': columnMap[i][1], 'checked': true});
values.push(this.getValueForColumn(model.getEach(columnMap[i][0]), columnMap[i][2])); //WRONG HERE
}
this.set('model_table', {});
let model_table = this.get('model_table');
Ember.set(model_table, 'values', values);
},
I am able to show in the template if I start doing a bunch of if's in the {{template}}, because I believe the template does some kind of binding I am not doing and it resolves later, but it was really ugly and nasty. I wanted to do something cleaner... that's why I'm posting here.
move your api calls to afterModel.
and
try something like this where you wait for promise to resolve and set it.
afterModel: function () {
var rsvp = Ember.RSVP.hash({
obj1: Ember.$.getJSON("/api/obj1"),
obj2: Ember.$.getJSON("/api/obj2"),
});
var ret = rsvp.then(function (resolve) {
console.log("Something" + resolve.obj1);
self.set('obj2', resolve.obj2);
});
return ret;
},
setupController: function (controller, model) {
this._super(controller, model);
controller.set('obj1', this.get('obj1'));
controller.set('obj2, this.get('obj2'));
}

Detail page of split app without model

In my split app the detail view does not bind any model.
In the component.js I instantiate a named model like this:
// creation and setup of the oData model
var oConfig = {
metadataUrlParams: {},
json: true,
defaultBindingMode : "TwoWay",
defaultCountMode : "Inline",
useBatch : false
}
// ### tab-employee ###
var oModelEmpl = new sap.ui.model.odata.v2.ODataModel("/sap/opu/odata/sap/EMP_SRV"), oConfig);
oModelEmpl.attachMetadataFailed(function() {
this.getEventBus().publish("Component", "MetadataFailedEMPL");
}, this);
this.setModel(oModelEmpl, "EMPL");
The method onSelect in der master-view controller is fired by clicking on an listitem.
onSelect: function(oEvent) {
this.showDetail(oEvent.getParameter("listItem") || oEvent.getSource());
}
This will call the method showDetail
showDetail: function(oItem) {
var bReplace = jQuery.device.is.phone ? false : true;
this.getRouter().navTo("detail", {
from: "master",
entity: oItem.getBindingContext('EMPL').getPath().substr(1),
}, bReplace);
},
In the controller of the detail-view I've these two methods for updating the binding. onRouteMatched calls bindView, where I get the error-message TypeError: oView.getModel(...) is undefined.
onRouteMatched: function(oEvent) {
var oParameters = oEvent.getParameters();
jQuery.when(this.oInitialLoadFinishedDeferred).then(jQuery.proxy(function() {
var oView = this.getView();
if (oParameters.name !== "detail") {
return;
}
var sEntityPath = "/" + oParameters.arguments.entity;
this.bindView(sEntityPath);
}, this));
},
bindView: function(sEntityPath) {
var oView = this.getView();
oView.bindElement(sEntityPath);
//Check if the data is already on the client
if (!oView.getModel().getData(sEntityPath)) {
// Check that the entity specified was found.
oView.getElementBinding().attachEventOnce("dataReceived", jQuery.proxy(function() {
var oData = oView.getModel().getData(sEntityPath);
if (!oData) {
this.showEmptyView();
this.fireDetailNotFound();
} else {
this.fireDetailChanged(sEntityPath);
}
}, this));
} else {
this.fireDetailChanged(sEntityPath);
}
},
I've tried to implement this split app relative to the template generated by WebIDE. Any idea what is missing?
As you wrote yourself, you are creating a "named Model" with the name "EMPL".
In the Controller you have to use the same name to get the Model:
this.getView().getModel("EMPL");
Likewise when calling bindElement() you have to give the model name:
// Assuming sEntityPath = "/items/0"
this.getView().bindElement("EMPL>" + sEntityPath);

How to merge results of two queries using FirebaseArray [duplicate]

I am currently working on an app using firebase and angularJS (ionic). Basically this is a car management app, so you have people sharing their cars with others. I tried to structure the data as flat as possible to be efficient. My issue here is that if without problem I can display the list of the car_id of the different cars shared with the logged user, I can't find a way to display the list of cars shared with the user displaying the year and the model.
Thank you in advance for your help !
{
"rules": {
"users": {
".write": true,
"$uid": {
".read": "auth != null && auth.uid == $uid"
},
"cars": {
"car_id":true,
"role":true // Owner, borower...
}
},
"cars": {
"car_id":true,
"model":true,
"year":true
}
}
}
carapp.controller("carsController", function($scope, $firebaseObject, $ionicPopup, $ionicHistory) {
$ionicHistory.clearHistory();
$scope.list = function() {
frbAuth = frb.getAuth();
if(frbAuth) {
var userObject = $firebaseObject(frb.child("users/" + frbAuth.uid));
userObject.$bindTo($scope, "user");
$scope.cars = frb.child("cars");
}}
$scope.createCar = function() {
$ionicPopup.prompt({
model: 'Create a new car',
inputType: 'text'
})
.then(function(result) {
if(result !== "") {
var newCar = $scope.cars.push({
model: result
})
var newCarId = newCar.key();
$scope.user.cars.push({car_id: newCarId, role: "owner" });
} else {
console.log("Action not completed");
}
});
}
});
<div class="list">
<a ng-repeat="car in user.cars" >
<h2>{{car.car_id}}</h2> ----> works fine !
<h2>{{car.model}}</h2> ----> How to get this working ?
<h2>{{car.year}}</h2> ----> How to get this working ?
</a>
</div>
In the users/ path, begin by storing the list of cars by index, instead of in an array. So your structure would be:
{
"users": {
"kato": {
"cars": {
"DeLorean": true
}
}
},
"cars": {
"DeLorean": {
model: "DeLorean",
year: "1975"
}
}
}
To join this using AngularFire, you have several approaches available. An AngularFire-only solution might look like this, taking advantage of $extend:
app.factory('CarsByUser', function($firebaseArray) {
return $firebaseArray.$extend({
$$added: function(snap) {
return new Car(snap);
},
$$updated: function(snap) {
// nothing to do here; the value of the index is not used
},
$$removed: function(snap) {
this.$getRecord(snap.key()).destroy();
},
// these could be implemented in a manner consistent with the
// use case and above code, for simplicity, they are disabled here
$add: readOnly,
$save: readOnly
});
var carsRef = new Firebase(...).child('cars');
function Car(snap) {
// create a reference to the data for a specific car
this.$id = snap.key();
this.ref = carsRef.child(this.$id);
// listen for changes to the data
this.ref.on('value', this.updated, this);
}
Car.prototype.updated = function(snap) {
this.model = data.model;
this.year = data.year;
}
Car.prototype.destroy = function() {
this.ref.off('value', this.meta, this);
};
function readOnly() { throw new Error('This is a read only list'); }
});
app.controller('...', function($scope, CarsByUser, authData) {
// authenticate first, preferably with resolve
var ref = new Firebase(...).child(authData.uid);
$scope.cars = CarsByUser($scope);
});
For a more sophisticated and elegant approach, one could utilize NormalizedCollection and pass that ref into the AngularFire array:
app.controller('...', function($scope, $firebaseArray) {
var ref = new Firebase(...);
var nc = new Firebase.util.NormalizedCollection(
ref.child('users/' + authData.uid),
ref.child('cars')
)
.select('cars.model', 'cars.year')
.ref();
$scope.cars = $firebaseArray(nc);
});

AngularFire Check if item of same title exists

I have Firebase entries in /items with the properties title and points. I am trying to check to see if an item of the same title exists before entering a new one.
This is what I have but it does not happen:
app.controller('ItemController', function($scope, FURL, $firebase, $location, toaster) {
var ref = new Firebase(FURL);
var fbItems = $firebase(ref.child('items')).$asArray();
$scope.addItem = function(item) {
// var iItem = $scope.item.title;
var userId = $scope.item.title;
checkIfUserExists(userId);
};
function userExistsCallback(userId, exists) {
if (exists) {
alert('user ' + userId + ' exists!');
} else {
alert('user ' + userId + ' does not exist!');
}
}
function checkIfUserExists(userId) {
ref.child('items').once('value', function(snapshot) {
var exists = (snapshot.val() !== null);
userExistsCallback(userId, exists);
});
}
});
The Realtime Database is a key/value JSON database. This means that if you store a title name as a key, it will be super quick to look it up.
Take the following data for example.
{
"items": {
"title-1": {
"something": "foo"
},
"title-2": {
"something": "baz"
}
}
}
Now let's say we want to check to see if title-2 exists. We can do this with an easy read.
function checkForTitle(title, cb) {
var itemsRef = new Firebase('<my-firebase-app>/items');
var titleRef = itemRef.chld(title).once('value', function(snap) {
cb(snap.exists());
});
}
checkForTitle('title-2', function(doesExist) {
console.log(doesExist); // true
});
To make sure the check happens on the server, you can write a Security Rule for it. Or, better yet use the new Bolt Compiler.
{
"items": {
"$item": {
".write": "!data.exists()" // don't overwrite existing data
}
}
}
You should upgrade your AngularFire version. I noticed you're using $firebase().$asArray which means you're on the 0.9 version of AngularFire, which is unsupported. Look into upgrading to the 1.0+ version which is officially support by Firebase.

Categories