Accessing Related Object Data in AngularFire for Firebase - angularjs

I want to relate a business to another business in my Firebase data and then use AngularFire to ng-repeat through the related businesses. My Firebase data is structured like so:
{
"businesses" : {
"-KQ1ggyrnYGYL1084Htz" : {
"name" : "Some Business",
"cat" : "Food",
"related" : {
"-KQ1hFH0qrvKSrymmn9d" : true,
In my JS I have the following factory and controller:
app.factory("Businesses", ['$firebaseArray', function ($firebaseArray) {
var stuff = firebase.database().ref().child("businesses");
var Businesses = $firebaseArray(stuff);
return Businesses;
}])
And my controller:
app.controller('businessCtrl', ['$scope','$firebaseArray','Businesses', function ($scope, $firebaseArray, Businesses) {
$scope.businesses = Businesses;
In the HTML I then loop through the "Related" businesses using an ng-repeat like so:
<span>Related:</span>
<a ng-repeat="item in businesses.related" ui-sref="shop.item({item: item.link})">
<div class="chip">
<img ng-src="{{item.card}}" alt="Contact Person">
{{item.name}}
</div>
</a>
But I seem unable to access any data such as "name" from my {{item.name}}. What do I need to do to be able to access the data of the referenced business?

If you want to access the data item.name then use item in businesses instead of item in businesses.related
When using item in businesses.related you are getting the data as item.-KQ1hFH0qrvKSrymmn9d

Alright, so I discovered how to do this sometime later. It may have worked the way I was trying, but it's not the recommended approach. For one, denormalization in Firebase is normal. Meaning, instead of placing the related businesses in a sub-object I need to create a new object like so:
businesses
-BusinessKey
related
-BusinessKey
-RelatedBusinessKey
Then get the data like so:
$scope.related = [];
const businessKey = $scope.finalItem.$id;
const rootRef = firebase.database().ref();
const discoverRef = rootRef.child('discover');
const businessRef = rootRef.child('businesses');
function getRelatedContent(key, cb) {
relatedRef.child(key).on('child_added', snap => {
let businesRef = businessRef.child(snap.key);
businesRef.once('value', cb);
});
}
getRelatedContent(businessKey, snap => {
var snapVal = snap.val();
$scope.related.push(snapVal);
});

Related

Reading child of an automated unique ID in a nested ng-repeat

How do I get data of a child of an automated unique ID in a nested ng-repeat, if the ID is not in the first level?
My firebase data set is structured as such (note: I do not know the amount or values of users, dates or unique IDs in that data set):
John:[
10-12-2015: {
"-JcFXid1A2G8EM7A_kwc": {
"description" : "hi",
"timestamp": "1449751857810"},
"-JcFZP5FNtL4Yj6nja_7": {
"description" : "this",
"timestamp": "144975185345"},
},
11-12-2015: {
"-JcFtGoZL7J-CCIjTYcL": {
"description" : "is",
"timestamp": "14497518513715"},
"-JcFXid1A2G8EM7A_kwc": {
"description" : "me",
"timestamp": "1449751846956"},
},
]
Thomas: [ ... ]
My HTML code is:
<ion-list>
<ul class="list">
<li ng-repeat="day in list">
<ion-item class="item-divider">{{day.$id}}</ion-item>
// That works as my output is "10-12-2015, etc"
<li ng-repeat="key in day">
// This does not work
<ion-item>{{key.description}}</ion-item>
</li>
</li>
</ul>
</ion-list>
I am new to Firebase and Angular so my apologies if I am missing something obvious. I also understand that Firebase recommends flat hierarchies, but sorting it by user and date is important for data analysis I need to do. But I would be open to suggestions.
FYI - My controller looks like this:
.controller('summaryCtrl', ['$scope','$state','$firebase','$firebaseArray','SessionData', '$ionicPopup', function($scope,$state,$firebase,$firebaseArray,SessionData,$ionicPopup){
var firebaseObj = new Firebase("https://id.firebaseio.com/" + userid);
var fbArray = $firebaseArray(firebaseObj);
fbArray.$loaded()
.then(function(){
$scope.list = fbArray;
})
}])
Thanks.
You don't need to use .$loaded(), the $firebaseArray will take care of triggering $digest when the data is loaded. The .$loaded() promise is useful for resolving in routes.
In your case though you need to go down into the day key to create the array. Try a data structure like below:
{
"userMessages": {
"$uid": {
"$message_id": {
"timestamp": 1450048003760 // use a timestamp
}
}
}
}
This way you can get messages per user by: /userMessages/$uid/. Then using a query you can get them by date.
You can create factory to simplify this.
Don't mind the code structure below, it uses the Angular Styleguide.
angular.module('app', ['firebase'])
.constant('FirebaseUrl', '<my-firebase-app>')
.service('rootRef', ['FirebaseUrl', Firebase])
.factory('messages', Messages)
.controller('SummaryCtrl', SummaryCtrl);
function Messages(rootRef, $firebaseArray) {
return function Messages(userid, date) {
// turn the date into a timestamp
var timestamp = date.getTime();
var messagesRef = rootRef.child('userMessages').child(userid);
// query for messages on that day
var query = messagesRef.orderByChild('timestamp').endAt(timestamp);
return $firebaseArray(query);
};
}
function SummaryCtrl($scope, messages, rootRef) {
var user = rootRef.getUser()
var today = new Date();
$scope.messages = messages(user.uid, today);
}

How to get specific element and orderby priority in Firebase and Angular

I have a firebase reference like this:
$scope.lessons = $firebaseArray(firebase.child('Lessons').orderByChild('courseID').equalTo($state.params.courseID));
I'm outputting the lessons like this:
<ul>
<li class="lesson" ng-if="lesson.moduleID == module.$id" ng-repeat="lesson in lessons" lessonid="{{lesson.$id}}">
<lessonedit></lessonedit>
</li>
</ul>
I need to order the lessons by priority, but when I add the orderByPriority() function to the end of the firebase reference it says I can only use one orderBy call.
I tried the filter lesson in lessons | orderByPriority but it says that filter does not exist?
Currently, you can't mix .orderByPriority() and .orderByChild() as they are different order by functions.
However, you can still solve your problem by only using .orderByPriority(), if you re-structure your data.
Structuring your data a specific way allows you query your data, as if you could use two orderBy functions.
In your case you have a location of "Lessons" that all have a push-id key of their "lesson id": /lessons/$lessonID.
You could change your structure to key off of the courseId and then the lesson_id: /lessions/$courseID/$lessonID.
The data would look like this:
{
"lessons": {
"1": {
"-K4NQTsjo3iswX4PhKUw": {
title: "My Course"
},
"-K4NAWsjo5iswX4Jk4fa": {
title: "Another Course"
}
}
"2": {
"-K4NQTsjo3iswX4PhKUw": {
title: "My Course"
},
"-K4NQPjMZVCoFRYc_1v5": {
title: "Intro to Data Structure"
}
}
}
}
Now since the key uses the courseID, we can order by by both courseID and priority:
var courseID = $state.params.courseID;
var ref = new Firebase('<my-firebase-app>.firebaseio.com/lessons');
var query = ref.child(courseID).orderByPriority();
var syncArray = $firebaseArray(query);
You can store this in a factory in your AngularJS code.
angular.module('app', ['firebase')
.constant('FirebaseUrl', '<my-firebase-app>')
.service('RootRef', ['FirebaseUrl', Firebase])
.factory('Lessons', Lessons)
.controller('MainCtrl', MainCtrl);
function Lessons(RootRef) {
return function Lessons(courseID) {
var ref = RootRef.child('Lessons');
var query = ref.child(courseID).orderByPriority();
return $firebaseArray(query);
}
}
function MainCtrl($scope, $state, Lessons) {
var courseID = $state.params.courseID;
$scope.lessons = Lessons(courseID);
}
If Priority is an attribute of lesson, you can use the angular filter orderBy like this:
<ul>
<li class="lesson"
ng-if="lesson.moduleID == module.$id"
ng-repeat="lesson in lessons | orderBy:'Priority'"
lessonid="{{lesson.$id}}">
<lessonedit></lessonedit>
</li>
</ul>
Using Firebase's new Query API, you can do this with:
var ref = new Firebase('https://your.firebaseio.com/');
ref
.orderBy('genre')
.startAt('comedy').endAt('comedy')
.orderBy('lead')
.startAt('Jack Nicholson').endAt('Jack Nicholson')
.on('value', function(snapshot) {
console.log(snapshot.val());
});
Note that the new Query API is still in beta, so things may change before it's officially released.
See this jsbin for a similar query on Firebase's sample data set.
You can check the original Post of Frank van Puffelen here

How to get the underlying Backbone Collection in a Knockback CollectionObservable from the $data context object with nested templates

Taking the following code snippet as a quick example:
var Animal = Backbone.Model.extend();
var Zoo = Backbone.Collection.extend({ model: Animal });
var tiger = new Animal({ name: "tiger" });
var zoo = new Zoo(tiger);
var viewModel = {
tiger: kb.viewModel(tiger);
zoo: kb.collectionObservable(zoo);
}
ko.applyBindings(viewModel);
from the $data context you can get a reference to the tiger model:
tiger === $data.tiger().__kb.object;
or
tiger === $data.zoo()[0].__kb.object;
and I assume it exists somewhere on this dependantObservable function, but I can't seem to find the reference to the original Backbone Collection
$data.zoo
Does anyone have any idea of how to get at the original Backbone Collection?
Also, bonus points if you can tell me of any way to get at the Backbone Collection if the viewmodel is this instead:
viewModel = kb.collectionObservable(zoo)
the challenge here is that $data contains the results of the evaluated dependantObservable function.
EDIT
After receiving a perfectly valid answer to the question above I realized that my problem only occurs in my more complicated binding with nested templates:
The templates look like this:
<!-- outer template -->
<script type="text/html" id="tmpl-outer">
<button data-bind="click: $root.outerContext">Outer Context</button>
<div data-bind="template: { name: 'tmpl-inner', data: collection }"></div>
</script>
<!-- inner template -->
<script type="text/html" id="tmpl-inner">
<button data-bind="click: $root.innerContext">Inner Context</button>
<div data-bind="foreach: $data">
<button data-bind="click: $root.modelContext">Model Context</button>
</div>
</script>
Model and View-Model:
var model = new Backbone.Model();
var collection = new Backbone.Collection(model);
var viewModel = {
collection: kb.collectionObservable(collection),
outerContext: function (data) {
console.log(data.collection.collection() === collection);
},
innerContext: function (data) {
console.log("??????? === collection");
},
modelContext: function (data) {
console.log(data.model() === model);
}
};
ko.applyBindings(viewModel);
And finally, somewhere to render everything:
<body>
<div data-bind="template: { name: 'tmpl-outer' }"></div>
</body>
So, my initial question that I over-simplified my example for should have been: how do I get at the underlying collection on the line:
console.log("??????? === collection");
It appears that the collection in this context has been converted to a simple KnockOut observable array - there doesn't seem to be any of the important KnockBack properties.
You can get the underlying collection / model by using the getters on instances of kb.CollectionObservable and kb.ViewModel.
var collection = new Backbone.Collection(),
view_models = kb.collectionObservable(collection),
reference = view_models.collection();
console.log(collection === reference);
You can do the same with instances of kb.viewModel
var model = new Backbone.Model({ id : 1 }),
view_model = kb.viewModel(model),
reference = view_model.model();
console.log(model === reference);
You can access the collection/model as well from $data by calling the getters in the data-binds, though I really can't see any need at all to do this if you use factory view_models for the collection allowing you to define any number of specific computeds / observables for each vm.
var model = new Backbone.Model({ id : 1 });
var collection = new Backbone.Collection(model);
var AnimalViewModel = kb.ViewModel.extend({
constructor: function(model) {
kb.ViewModel.prototype.constructor.call(this, model, {});
return this;
// Custom code per vm created
}
});
var view_model = {
zoo : kb.collectionObservable(collection, {
view_model : AnimalViewModel
});
}
In the end I found that I had to go via the parent to get the collection. I don't like this level of indirection, but I can't find any way around it.
The view-model now has this function in it:
doSomethingWithUnderlyingCollection: function(collectionName, parentContext) {
var underlyingCollection = parentContext.model().get(collectionName);
// do something with the underlying collection here, e.g. add a model.
}
And then to call the method from the template:
<button data-bind="click: function() { $root.doSomethingWithUnderlyingCollection('MyCollection', $parent); }">Add</button>

How to get a single item from a GoInstant collection?

How do you get a single item from a GoInstant GoAngular collection? I am trying to create a typical show or edit screen for a single task, but I cannot get any of the task's data to appear.
Here is my AngularJS controller:
.controller('TaskCtrl', function($scope, $stateParams, $goKey) {
$scope.tasks = $goKey('tasks').$sync();
$scope.tasks.$on('ready', function() {
$scope.task = $scope.tasks.$key($stateParams.taskId);
//$scope.task = $scope.tasks.$key('id-146b1c09a84-000-0'); //I tried this too
});
});
And here is the corresponding AngularJS template:
<div class="card">
<ul class="table-view">
<li class="table-view-cell"><h4>{{ task.name }}</h4></li>
</ul>
</div>
Nothing is rendered with {{ task.name }} or by referencing any of the task's properties. Any help will be greatly appreciated.
You might handle these tasks: (a) retrieving a single item from a collection, and (b) responding to a users direction to change application state differently.
Keep in mind, that a GoAngular model (returned by $sync()) is an object, which in the case of a collection of todos might look something like this:
{
"id-146ce1c6c9e-000-0": { "description": "Destroy the Death Start" },
"id-146ce1c6c9e-000-0": { "description": "Defeat the Emperor" }
}
It will of course, have a number of methods too, those can be easily stripped using the $omit method.
If we wanted to retrieve a single item from a collection that had already been synced, we might do it like this (plunkr):
$scope.todos.$sync();
$scope.todos.$on('ready', function() {
var firstKey = (function (obj) {
for (var firstKey in obj) return firstKey;
})($scope.todos.$omit());
$scope.firstTodo = $scope.todos[firstKey].description;
});
In this example, we synchronize the collection, and once it's ready retrieve the key for the first item in the collection, and assign a reference to that item to $scope.firstTodo.
If we are responding to a users input, we'll need the ID to be passed from the view based on a user's interaction, back to the controller. First we'll update our view:
<li ng-repeat="(id, todo) in todos">
{{ todo.description }}
</li>
Now we know which todo the user want's us to modify, we describe that behavior in our controller:
$scope.todos.$sync();
$scope.whichTask = function(todoId) {
console.log('this one:', $scope.todos[todoId]);
// Remove for fun
$scope.todos.$key(todoId).$remove();
}
Here's a working example: plunkr. Hope this helps :)

implement embedding comments like twitter in angularjs

i know in Angular world it is better to bind data than manipulate dom elements. but i can't figure out a way to implement the 'in timeline, click a tweet, load replies, click another tweet load another replies' effects.
here is some code run into my thoughts:
<div class="tweet" ng-repeat="tweet in tweets">
<div class="tweet-content">{{tweet}}</div>
<a class="button" ng-click="loadreplay()">load reply</a>
<div class="reply-container">{{reply}}</div>
</div>
if i write controller like this
app.controller('Test', function($scope){
$scope.tweets = ["foo", "bar"];
$scope.loadreplay = function(){
$scope.reply = "reply";
}
});
then all {{reply}} fields will be filled with 'reply', so in this condition, is manipulate the dom elements the only resolution? or some more "angular" way?
Use a appropriate schema for your data/model. Considering that you would store not only the text but at least something like a ID you would use an object anyway. So think about something like this:
$scope.tweets = [
{ id:1, txt: 'foo' },
{ id:2, txt: 'bar' }
]
Then you could store the individual replies in that object as well:
$scope.loadreply = function(tweet) {
tweet.reply = 'Reply';
}
Note: In this function you could then also use the ID to e.g. fetch the tweets from the server like this:
$scope.loadreply = function(tweet) {
tweet.reply = LoadReplies(tweet.id);
}
You would then use the tweet specific reply attribute for display:
<div ng:repeat="tweet in tweets">
<div>{{tweet.txt}}</div>
<a ng:click="loadreply(tweet)">load reply</a>
<div>{{tweet.reply}}</div>
</div>
See this fiddle for a working demo: http://jsfiddle.net/XnBrp/

Resources