Firestore get array from collection with id's to use in Polymer dom-repeat - google-cloud-firestore

im trying to use Firestore with Polymer, I obtain an array to send it to polymer in a dom-repeat like this:
var query=db.collection("operaciones");
db.collection("operaciones")
.onSnapshot((querySnapshot) => {
querySnapshot.forEach(function(doc) {
});
that.operacionesPorCliente=Array.from(querySnapshot.docs.map(doc=>doc.data()));
});
console.log (that.operacionesPorCliente); // this works but the ID doesnt exist here....
}
that works but that array doesnt contain the id from firestore, the problem is that I need that ID to update the data :( but it isn't in the array
Hope I explain my self, any help?

I make a Polymer Element (Polymer 3) to keep Firebase Firestore data synchronized. This component has a dom-repeat template element, to show the always-fresh collection data.
I think this will answer your question
import {html, PolymerElement} from '#polymer/polymer/polymer-element.js';
import {} from '#polymer/polymer/lib/elements/dom-repeat.js';
/**
* #customElement
* #polymer
*/
class FirebaseFirestoreApp extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<h1>Firestore test</h1>
<template is="dom-repeat" items="[[elems]]">
<p>[[item.$id]] - [[item.name]]</p>
</template>
`;
}
static get properties() {
return {
elems: {
type: Array,
value: function() {
return [];
}
}
};
}
ready() {
super.ready();
var db = firebase.firestore();
const settings = {timestampsInSnapshots: true};
db.settings(settings);
db.collection("operaciones").onSnapshot((querySnapshot) => {
querySnapshot.docChanges().forEach((change) => {
if (change.type === "added") {
let newElem = this.makeElem(change);
this.push('elems', newElem);
}
if (change.type === "modified") {
let modifiedElement = this.makeElem(change);
let index = this.getElemIndex(change.doc.id);
this.set(`elems.${index}`, modifiedElement);
}
if (change.type === "removed") {
let deletedElement = this.getElemIndex(change.doc.id);
this.splice('elems', deletedElement, 1);
}
});
});
}
makeElem(change) {
let data = change.doc.data();
data.$id = change.doc.id;
return data;
}
getElemIndex(id) {
let index = this.elems.findIndex((elem) => {
if(elem.$id == id) {
return true;
}
});
return index;
}
}
window.customElements.define('firebase-firestore-app', FirebaseFirestoreApp);
The sync systems should works with all kind of Firebase Firestore collections, but the template dom-repeat suposses there is a property called "name" in the objects inside the collection.
So, the collection in the Firebase console looks like that.
Firebase console screenshot

I suggest you use Polymerfire Polymerfire that are polymer components for Firebase, but if you want to do it in javascript, can get the id directly in the doc: doc.id().

Related

Detecting changes in lit sub-properties

I've made a simplified reduction of the issue I'm having with lit. Some of the state of my component is held in an object. I'd like to be able to detect and react to a change in a sub-property with some fine control. Here's the example:
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
#customElement("my-app")
export class App extends LitElement {
#state({
hasChanged(newVal: any, oldVal: any) {
console.log(`[hasChanged] new fruit is: ${newVal?.fruit}`);
console.log(`[hasChanged] old fruit is: ${oldVal?.fruit}`);
return true;
}})
data = {
fruit: "apples",
weather: "rain",
};
constructor() {
super();
setTimeout(() => {
this.data.fruit = "bananas";
this.data = { ...this.data };
}, 3000);
setTimeout(() => {
this.data.weather = "snow";
this.data = { ...this.data };
}, 6000);
}
render() {
return html`
Weather: ${this.data.weather} Fruit: ${this.data.fruit}
`;
}
shouldUpdate(changedProperties: any) {
// I only want to update if the weather is changing, not the fruit
console.log(
`new weather is: ${changedProperties.get("data")?.weather}`
);
console.log(`current weather is: ${this.data.weather}`);
console.log(`new fruit is: ${changedProperties.get("data")?.fruit}`);
console.log(`current fruit is: ${this.data.fruit}`);
console.log("");
if (changedProperties.get("data")?.weather !== this.data.weather) {
return true;
} else {
return false;
}
}
}
When shouldUpdate fires, the component's evaluation of the sub-property values has already updated. So I can't compare changedProperties.get("data")?.weather with this.data.weather to see if it has changed.
[Update] at michaPau's suggestion I looked into the hasChanged method - unfortunately this also gives back the same values for oldVal and newVal when a change is triggered.
Does anyone know a new approach or fix? It works as expected if I split that state object into two separate state variables, but objects and properties works so much better in keeping code readable. Passing more than 4 or 5 properties into a component starts getting messy, in my opinion.
I don't know exactly, what you are doing wrong, but your code seems to work actually, but yes as others mentioned, you should use the spread operator together with the assignment operation:
<script type="module">
import {
LitElement,
html,
css
} from "https://unpkg.com/lit-element/lit-element.js?module";
class MyContainer extends LitElement {
static get styles() {
return css`
.wrapper {
min-height: 100px;
min-width: 50%;
margin: 5em;
margin-top: 0;
padding: 10px;
background-color: lightblue;
}
`;
}
render() {
return html`
<div class="wrapper">
<slot></slot>
</div>
`;
}
}
class MyItem extends LitElement {
static properties = {
data: {
state: true,
hasChanged: (newVal, oldVal) => {
console.log(`[hasChanged] new fruit is: ${newVal?.fruit}`);
console.log(`[hasChanged] old fruit is: ${oldVal?.fruit}`);
return true;
}
}
};
constructor() {
super();
this.data = {
fruit: "apples",
weather: "rain"
};
setTimeout(() => {
this.data = { ...this.data, fruit: 'bananas' };
}, 3000);
setTimeout(() => {
this.data = { ...this.data, weather: "snow" };
}, 6000);
}
render() {
return html`
<div style="background-color: ${this.getBackgroundColor()}">
Weather: ${this.data.weather} Fruit: ${this.data.fruit}
</div>
`;
}
getBackgroundColor() {
if(this.data.weather === 'snow') return 'white';
else if(this.data.fruit === 'bananas') return 'yellow';
return 'blue';
}
shouldUpdate(changedProperties) {
// I only want to update if the weather is changing, not the fruit
console.log(`new weather is: ${changedProperties.get("data")?.weather}`);
console.log(`current weather is: ${this.data.weather}`);
console.log(`new fruit is: ${changedProperties.get("data")?.fruit}`);
console.log(`current fruit is: ${this.data.fruit}`);
console.log("");
if (changedProperties.get("data")?.weather !== this.data.weather) {
return true;
} else {
return false;
}
}
}
customElements.define("my-container", MyContainer);
customElements.define("my-item", MyItem);
</script>
<my-container>
<my-item></my-item>
</my-container>
If you want to see also the change to yellow change your code in the shouldUpdate function to return true if changedProperties.get("data") is not undefined.

Firestore method snapshotChanges() for collection

Following is the code provided in Collections in AngularFirestore.
export class AppComponent {
private shirtCollection: AngularFirestoreCollection<Shirt>;
shirts: Observable<ShirtId[]>;
constructor(private readonly afs: AngularFirestore) {
this.shirtCollection = afs.collection<Shirt>('shirts');
// .snapshotChanges() returns a DocumentChangeAction[], which contains
// a lot of information about "what happened" with each change. If you want to
// get the data and the id use the map operator.
this.shirts = this.shirtCollection.snapshotChanges().map(actions => {
return actions.map(a => {
const data = a.payload.doc.data() as Shirt;
const id = a.payload.doc.id;
return { id, ...data };
});
});
}
}
Here method snapshotChanges() returns observable of DocumentChangeAction[]. So why using a map to read it when it has only one array and it will loop only one time?

Handling tab for lists in Draft.js

I have a wrapper around the Editor provided by Draft.js, and I would like to get the tab/shift-tab keys working like they should for the UL and OL. I have the following methods defined:
_onChange(editorState) {
this.setState({editorState});
if (this.props.onChange) {
this.props.onChange(
new CustomEvent('chimpeditor_update',
{
detail: stateToHTML(editorState.getCurrentContent())
})
);
}
}
_onTab(event) {
console.log('onTab');
this._onChange(RichUtils.onTab(event, this.state.editorState, 6));
}
Here I have a method, _onTab, which is connected to the Editor.onTab, where I call RichUtil.onTab(), which I assume returns the updated EditorState, which I then pass to a generic method that updates the EditorState and calls some callbacks. But, when I hit tab or shift-tab, nothing happens at all.
So this came up while implementing with React Hooks, and a google search had this answer as the #2 result.
I believe the code OP has is correct, and I was seeing "nothing happening" as well. The problem turned out to be not including the Draft.css styles.
import 'draft-js/dist/Draft.css'
import { Editor, RichUtils, getDefaultKeyBinding } from 'draft-js'
handleEditorChange = editorState => this.setState({ editorState })
handleKeyBindings = e => {
const { editorState } = this.state
if (e.keyCode === 9) {
const newEditorState = RichUtils.onTab(e, editorState, 6 /* maxDepth */)
if (newEditorState !== editorState) {
this.handleEditorChange(newEditorState)
}
return
}
return getDefaultKeyBinding(e)
}
render() {
return <Editor onTab={this.handleKeyBindings} />
}
The following example will inject \t into the current location, and update the state accordingly.
function custKeyBindingFn(event) {
if (event.keyCode === 9) {
let newContentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
'\t'
);
setEditorState(EditorState.push(editorState, newContentState, 'insert-characters'));
event.preventDefault(); // For good measure. (?)
return null;
}
return getDefaultKeyBinding(event);
}

Add new data from restful api to angularjs scope

I'm trying to create a list with endless scroll in angularjs. For this I need to fetch new data from an api and then append it to the existing results of a scope in angularjs. I have tried several methods, but none of them worked so far.
Currently this is my controller:
userControllers.controller('userListCtrl', ['$scope', 'User',
function($scope, User) {
$scope.users = User.query();
$scope.$watch('users');
$scope.orderProp = 'name';
window.addEventListener('scroll', function(event) {
if (document.body.offsetHeight < window.scrollY +
document.documentElement.clientHeight + 300) {
var promise = user.query();
$scope.users = $scope.users.concat(promise);
}
}, false);
}
]);
And this is my service:
userServices.factory('User', ['$resource',
function($resource) {
return $resource('api/users', {}, {
query: {
method: 'GET',
isArray: true
}
});
}
]);
How do I append new results to the scope instead of replacing the old ones?
I think you may need to use $scope.apply()
When the promise returns, because it isnt
Part of the angular execution loop.
Try something like:
User.query().then(function(){
$scope.apply(function(result){
// concat new users
});
});
The following code did the trick:
$scope.fetch = function() {
// Use User.query().$promise.then(...) to parse the results
User.query().$promise.then(function(result) {
for(var i in result) {
// There is more data in the result than just the users, so check types.
if(result[i] instanceof User) {
// Never concat and set the results, just append them.
$scope.users.push(result[i]);
}
}
});
};
window.addEventListener('scroll', function(event) {
if (document.body.offsetHeight < window.scrollY +
document.documentElement.clientHeight + 300) {
$scope.fetch();
}
}, false);

Creating new Meteor collections on the fly

Is it possible to create new Meteor collections on-the-fly? I'd like to create foo_bar or bar_bar depending on some pathname which should be a global variable I suppose (so I can access it throughout my whole application).
Something like:
var prefix = window.location.pathname.replace(/^\/([^\/]*).*$/, '$1');
var Bar = new Meteor.Collection(prefix+'_bar');
The thing here is that I should get my prefix variable from URL, so if i declare it outside of if (Meteor.isClient) I get an error: ReferenceError: window is not defined. Is it possible to do something like that at all?
Edit : Using the first iteration of Akshats answer my project js : http://pastie.org/6411287
I'm not entirely certain this will work:
You need it in two pieces, the first to load collections you've set up before (on both the client and server)
var collections = {};
var mysettings = new Meteor.Collection('settings') //use your settings
//Startup
Collectionlist = mysettings.find({type:'collection'});
Collectionlist.forEach(function(doc) {
collections[doc.name] = new Meteor.Collection(doc.name);
})'
And you need a bit to add the collections on the server:
Meteor.methods({
'create_server_col' : function(collectionname) {
mysettings.insert({type:'collection', name: collectionname});
newcollections[collectionname] = new Collection(collectionname);
return true;
}
});
And you need to create them on the client:
//Create the collection:
Meteor.call('create_server_col', 'My New Collection Name', function(err,result) {
if(result) {
alert("Collection made");
}
else
{
console.log(err);
}
}
Again, this is all untested so I'm just giving it a shot hopefully it works.
EDIT
Perhaps the below should work, I've added a couple of checks to see if the collection exists first. Please could you run meteor reset before you use it to sort bugs from the code above:
var collections = {};
var mysettings = new Meteor.Collection('settings')
if (Meteor.isClient) {
Meteor.startup(function() {
Collectionlist = mysettings.find({type:'collection'});
Collectionlist.forEach(function(doc) {
eval("var "+doc.name+" = new Meteor.Collection("+doc.name+"));
});
});
Template.hello.greeting = function () {
return "Welcome to testColl.";
};
var collectionname=prompt("Enter a collection name to create:","collection name")
create_collection(collectionname);
function create_collection(name) {
Meteor.call('create_server_col', 'tempcoll', function(err,result) {
if(!err) {
if(result) {
//make sure name is safe
eval("var "+name+" = new Meteor.Collection('"+name+"'));
alert("Collection made");
console.log(result);
console.log(collections);
} else {
alert("This collection already exists");
}
}
else
{
alert("Error see console");
console.log(err);
}
});
}
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
Collectionlist = mysettings.find({type:'collection'});
Collectionlist.forEach(function(doc) {
collections[doc.name] = new Meteor.Collection(doc.name);
});
});
Meteor.methods({
'create_server_col' : function(collectionname) {
if(!mysettings.findOne({type:'collection', name: collectionname})) {
mysettings.insert({type:'collection', name: collectionname});
collections[collectionname] = new Meteor.Collection(collectionname);
return true;
}
else
{
return false; //Collection already exists
}
}
});
}
Also make sure your names are javascript escaped.
Things got much easier:
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
db.createCollection("COLLECTION_NAME", (err, res) => {
console.log(res);
});
Run this in your server method.