rebind properties in model, emberjs - data-binding

I have just started to use ember.js. I have two models in my application. One that holds data and one that holds this data edited by user. I bind them using one-way binding.
App.ViewModel = Ember.Object.create({
title:'title',
text:'text',
)};
App.EditModel = Ember.Object.create({
titleBinding: Ember.Binding.oneWay('App.ViewModel.title'),
textBinding: Ember.Binding.oneWay('App.ViewModel.text'),
)};
I let a user edit the data in EditModel model. But if the user discard the changes I want to be able to set the values back to the state before editing, ie. to the values in ViewModel.
Is there a way to rebind those properties? Or to manualy rise change event on properties in ViewModel so EditModel gets updated? Or any other approach to my problem?

You could create a custom Mixin which handles the reset for a model, see http://jsfiddle.net/pangratz666/CjB4S/
App.Editable = Ember.Mixin.create({
startEditing: function() {
var propertyNames = this.get('propertyNames');
var props = this.getProperties.apply(this, propertyNames);
this.set('origProps', props);
},
reset: function() {
var props = this.get('origProps');
Ember.setProperties(this, props);
}
});
App.myModel = Ember.Object.create(App.Editable, {
propertyNames: ['title', 'text'],
title: 'le title',
text: 'le text'
});
And later in the views you just invoke the startEditing when you want to take a snapshot of the current values and reset when you want to reset to the previous snapshot of the values.

Related

using watch function w/prop in Vue3 composition api

I have a component that renders a table of Inventoried computer equipment. Here is the relevant code for initial render:
let oEquiptByType = reactive({
Laptop: [],
iPad: [],
"Document Camera": [],
"Overhead Projector": [],
Chromebook: [],
Desktop: [],
MacBook: [],
Scanner: [],
});
// ======== Props =========== //
const props = defineProps({
propFormData: {},
});
// Now let's use Stein to retrieve the SS data
// eslint-disable-next-line no-unused-vars
const fetchSheetsData = function () {
const store = new SteinStore(
"https://api.steinhq.com/v1/storages/618e81028d29ba2379044caa"
);
store
.read("HS - Classrooms")
.then((data) => {
scrapDataHSClassrooms.value = data;
emptyRowsRemoved.value.forEach((item) => {
// Let's construct an object that separates equipment by type
// Check if property exists on oEquiptByType object
const exists = Object.prototype.hasOwnProperty.call(
oEquiptByType,
item["Equipment"]
);
// If item(row) is good lets push the row onto the corresponding Object Array
// in oEquiptByType. This will construct an object where each object property corresponds
// to an equipment category. And each oEquiptByType entry is an array where each array
// element is a row from the SS. e.g., oEquiptByType["Laptop"][3] is a row from
// SS and is a laptop.
if (exists) {
oEquiptByType[item["Equipment"]].push(item);
}
});
})
.catch((e) => {
console.error(e);
failure.value = true;
});
};
// =============== Called on component mount =============================== //
onMounted(fetchSheetsData);
The initial render is fine. Now I have a watcher on the prop so when someone submits a new item for the inventory I push that data onto the corresponding object array (ie, a laptop would be pushed onto the oEquiptByType[props.propFormData.Equipment] via oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
// ================================================================ //
// ======================= Watch effects ========================== //
// ================================================================ //
watch(props.propFormData, () => {
// Push the submitted form item onto the reactive
// oEquiptByType object array. This update of Vue state
// will then be injected into DOM and automagically update browser display.
oEquiptByType[props.propFormData.Equipment].push(props.propFormData);
});
This works fine for the first item I add to backend as you can see here with original and then adding first item :
and after first item added (a laptop)
Notice the oEquiptByType[props.propFormData.Equipment] has the new item added. Great.
But now when I add a second item (a MacBook) is added this is resulting state:
Notice the Macbook array has been updated but also the Laptop array's last item has been overwritten with the Mac book entry??? And this behavior continues for any additional items added from a user. I have read docs over and do not see anything that would explain this behavior. I'm hoping maybe someone with more than my limited experience with Vue can help me out. Any additional info needed please let me know. Thanks...
Update:
Put a JSON.Stringify in watch function
Update two:
here is lineage of prop.FormData-
we start in form-modal and emit the form data like:
emit("emiterUIUpdate", formAsPlainObject);
then catch the data in the parent App.vue:
<FormModal
v-show="isModalVisible"
#close="closeModal"
#emiterUIUpdate="updateUI"
>
<DisplayScrap :propFormData="formData" />
const formData = reactive({});
// Method to be called when there is an emiterUIUpdate event emiited
// from form-modal.vue #param(data) is the form data sent from the
// form submission via the event bus. We will then send this data back
// down to child display-scrap component via a prop.
const updateUI = (data) => {
Object.assign(formData, data);
};
and then as posted previous in display-scrap.vue the prop propFormData is defined and watched for in the watch function. hope that helps..
It seems like the watch is getting triggered more often than you expect.
Might be that changes to props.propFormData are atomic and every incremental change triggers changes to the props, which in turn triggers the watch.
Try console logging the value of props.propFormData with JSON.stringify to see what changes are triggering it.
What happens here:
Your form modal emits the emiterUIUpdate event on Ok or Save (button)
Parent takes the object emitted and use Object.assing to copy all properties of emitted object to a formData reactive object. Instead of creating completely new object, you are just replacing the values of all properties of that object all and over again
The formData object is passed by a prop to child component and whenever it changes, it is pushed to target array
As a result, you have a multiple references to same object (formData hold by a parent component) and all those references are to same object in memory. Every Object.assign will overwrite properties of this object and all references will reflect those changes (because all references are pointing to the same object in memory)
Note that this has nothing to do with Vue reactivity - this is simple JavaScript - value vs reference
There is no clear answer to what to do. There are multiple options:
Simplest (and not clearest)
just do not use Object.assign - create new object every time "Save" is clicked
change formData to a ref - const formData = ref({})
replace the value of that ref on emiterUIUpdate event - formData.value = { ...data }
your watch handler in the child will stop working because you are watching props in a wrong way - instead of watch(props.propFormData, () => { use watch(() => props.propFormData, () => {
Better solution
the data should be owned by parent component
when modal emits new data (Save), Parent will just add the newly generated object into a list
share the data with DisplayScraps component using a prop (this can be a simple list or a computed creating object similar to oEquiptByType)

Read OData Service and rebind List

I have a List, bound to an entityset from mainService. Same view contains a filter field. Once user enters some filtering criteria, the read should happen. I am already reading the OData entityset, results are coming back. But I have no luck to let the table be bound to that result
The binding in XML View
<List
id="list"
width="auto"
class="sapFDynamicPageAlignContent"
items= "{/ItProjHeadSet}"
busyIndicatorDelay="{masterView>/delay}"
noDataText="{masterView>/noDataText}"
mode="{= ${device>/system/phone} ? 'None' : 'SingleSelectMaster'}"
growing="true"
growingScrollToLoad="true"
updateFinished=".onUpdateFinished"
selectionChange=".onSelectionChange">
Then, when the GO button of the smart filter bar is clicked, I am triggering the onSearch event as follows:
onSearchProjects : function (oEvent) {
var oMasterPage = this.getView().byId("masterPage");
var that = this;
var aTokens = this._oMultiInput.getTokens();
var aMultiFilters = aTokens.map(function (oToken) {
var oProperties = oToken.data("range");
return new Filter({
path: oProperties.keyField,
operator: FilterOperator[oProperties.operation],
value1: oProperties.value1,
value2: oProperties.value2
});
});
oMasterPage.setBusy(true);
//Filter
this.getOwnerComponent().getModel().read("/ItProjHeadSet", {
filters: aMultiFilters,
success: function (oData) {
var list = that.getView().byId("list");
var projectModel = new JSONModel(oData);
var oModel = that.getView().getModel("/ItProjHeadSet");
oModel.setData(projectModel);
oModel.updateBindings(true);
error: function (oData) {
MessageToast.show(that.getResourceBundle().getText("noProjectsFetched"));
}
});
oMasterPage.setBusy(false);
},
The problem then is that although I am receiving the corresponding successful results in the read, the setData seems that it is happening to a different model than the one bound to the list.
Am I doing the right model update in the Success read?
Regards,
Martin
Solved by my own, by getting the list binding then applying filters:
List.getBinding("items").filter(aMultiFilters, "Application");
then there was no need to getOwnerComponent and all that

How to get all subscription data before rendering and then just update the dom when new data is received?

I currently havea widget which grabs hundreds of documents from the DB via subscription and then keep listening for new documents, so it can update a stock chart.
There is a problem tough, which is every time the data is updated the chart is updated, which causes a redraw.
This is a problem cause it's calling redraw hundreds of time at the beginning even tough it just need to "fetch all data then draw and wait for updates", the updates will then happen not very often, so then it would be ok to redraw.
my current code:
Template.nwidget.onRendered(function() {
return this.autorun(function() {
var data;
data = {};
data = Data.find({
type: 'my_type'
});
data = data.fetch();
return update(data);
});
});
For doing some after data subscription you can do like this:
Meteor.subscribe( 'collection', {
onStop: function( error /* optional */ ) {
// when the sub terminates for any reason,
// with an error argument if an error triggered the stop
},
onReady: function() {
// when ready
}
});
If you want to render page after the data subcribe then you can add waitOn in your router.
There is one more way to check where subscription is ready or not. If subscription is not ready you can show something else like a loading screen.
var handle = Meteor.subscribe( 'collection');
Tracker.autorun(function() {
if (handle.ready())
//write whatever you want to do here.
});
For the auto update in your view you can store the date in a reactive thing its may reactive var, Session or collection.
Then you can return there values from helper to view. And that will auto update your view.

Meteor.autorun() not working on client when insert occures

i have been knocking my head for 2 days now in that .
am creating a search engine, am creating queries dynamically using Meteor Framwork, the queries are working fine and when i search i can rebind the UI (Table in My Case) with the dynamic data query output.
however if an insert/update/delete operation occures the data object
and the UI (html Table) is not updating.
which means that the template is not re-rendered when the data object changes.
Template.search.rendered = function () {
Meteor.autorun(function() {
alarmsData = Alarms.find(getSearchSelector($('#searchTxt').val(), $('#startTimeTxt').val(), $('#endTimeTxt').val())).fetch()
console.log("rendered")
//alarmsData = Alarms.find({},{sort: {timestamp: "desc"} }).fetch();
searchControls(alarmsData)
getConsole(alarmsData, ".console")
$('#badge').html(alarmsData.length)
})
}
the get console function is just reading the array from teh search and creating an html table (this is working fine)
as for the begining i am creating a simple query as the default for my search. and then am changing this query whenever user changes the search criteria. i can notice that only the first instance of teh data object is kept and tracked for changes, so if the second search criteria resides within the first one, it's updating the UI, if not nothing happenes
i have used Meteor.autorun(function(){}) function however i traced it's execution with console.log and i can see it's no excuting when i insert data in the database for the same collection.
One, I believe you are trying to use Deps.autorun. Also, there is nothing in your autorun that seems to be dependent on a reactive source. Since alarmsData is taking a snapshot of data it won't care when Alarms has data changing.
Second, I would probably approach this with a redirect. I would compile my data, and redirect to the same page, allowing the server to handle the querying for me. This easily allows you to jump to this page from anywhere else with a prefilled query in the parameters (because the route would then handle it) and also gives a visual change to the navigation bar when a search has happened (just like every other search engine). You would do something like this on a button click:
var query = {},
path;
query.text = encodeURIComponent($('#searchTxt').val()),
query.start = encodeURIComponent($('#startTimeTxt').val()),
query.end = encodeURIComponent($('#endTimeTxt').val()),
// redirect to current path
path = Router.routes[Router.current().route.name].path({}, {
query: query
});
Router.go( path );
In your router you would just pass the query into your server and route as a data object (assuming you are using iron-router):
this.route( "search", {
path: "/search",
waitOn: function() {
return [
Meteor.subscribe( "searchAlarms", _.omit( this.params, "hash" ) ),
]
},
data: function () {
return { "query": _.omit( this.params, "hash" ) };
}
});
This will not only give you the query data that was used for the search (in your template) but the server can now handle the search for you! Your Alarms data now holds all of the documents needed to display to the user and you no longer need to subscribe to all your Alarms. This is also great because it is automatically reactive. So if a new Alarm matches your query filter it will automatically be passed down to the client and displayed to the user without needing to setup any extra dependencies/autoruns.
Note though, that if you are subscribing to Alarms elsewhere you will still need to do filtering client-side.
What a strange meteor code…
The "rendered" code method code is called once you will be rendering the search template
getSearchSelector($('#searchTxt').val() is not reactive, my advise is to use the session variable to put your search criteria inside and use this same session to inject the find parameters inside.
Are you looking for displaying all the alarms Data ?
function getAlarms()
{
var text = Session.get("text");
var from = Session.get("start");
var to = Session.get("end");
var filter = getSearchSelector(text, from, to);
return Alarms.find(filter);
}
Template.search.alarms = function () {
return getAlarms();
}
Template.search.alarmsCount = function () {
return getAlarms().count();
}
Template.search.events({
'keypress input[name=text]' : function(e,o)
{
var val = $("input[name= text]").val()
Session.set("text", val);
},
'keypress input[name=start]' : function(e,o)
{
var val = $("input[name=start]").val()
Session.set("start", val);
},
'keypress input[name=end]' : function(e,o)
{
var val = $("input[name=end]").val()
Session.set("end", val);
}
});
// And your template will look something like:
<template name="search">
Search alarms
<input type="text" name="text" placeholder="Enter your text here…"/>
<input type="text" name="start" placeholder="start time"/>
<input type="text" name="end" placeholder="end time/>
There is {{alarmsCount}} alarms(s);
{{#each alarms}}
Alarm object: {{.}}
{{/each}}
</template>
I Guess its Solved it by using Session.set & get, and automatically subscribing to the Serevr and send the dynamic Query.
Check the below Code
Template.zConsole.rendered = function () {
Session.set("obj", getSearchSelector($('#searchTxt').val(), $('#startTimeTxt').val(), $('#endTimeTxt').val()))
Deps.autorun(function (){
Meteor.subscribe("dynamicAlarms", Session.get("obj"))
console.log("Count from AutoRun ==> " + Alarms.find(Session.get("obj")).count())
})
}
on the server
Meteor.publish('dynamicAlarms',function (searchObj) {
return Alarms.find(searchObj)
})
& it works perfect with less code.

Prevent items in scope from writing to a different user's records

I was having success with using AngularFire in a scenario where there is one user on my application.
Now that I have authentication up and running, I'm noticing that assigning items to $scope.items is catastrophic when switching users, mainly due to the $scope failing to update correctly.
Reading directly from the docs...
var ref = new Firebase('https://<my-firebase>.firebaseio.com/items');
angularFire(ref, $scope, 'items');
I need these to be only the items of the currently authorized user. So currently, I do this (if there's a better way, don't hesitate to tell me!)
var ref = new Firebase('https://<my-firebase>.firebaseio.com/items/userId');
angularFire(ref, $scope, 'items');
I generate userId using auth.provider and auth.id, btw. Now that my items are namespaced in (let's say) user1
var ref = new Firebase('https://<my-firebase>.firebaseio.com/items/[user1id]');
angularFire(ref, $scope, 'items');
I add items to $scope.items
$scope.create = function(item) {
$scope.items.push(item)
/* Pretend the user adds these from the interface.
[
{ name: 'eenie' },
{ name: 'meenie' },
{ name: 'miney' },
{ name: 'moe' }
]
*/
}
The problem
Now if I just log out and login as someone else, magically that user has eenie meenie miney and moe because $scope.items held the array between logout and login.
I tried to set $scope.items = [] on logout event, but that actually empties all the records. I'm pulling my hair out. This is 0.001% of what I need to do in my project and it's taking my whole weekend.
Update New method
$scope.create = function() {
$scope.selectedDevice = {
name: 'New Device',
userId: $scope.user.provider + $scope.user.id
};
return $scope.devices.push($scope.selectedDevice);
};
$scope.$on('angularFireAuth:login', function(evt, user) {
var promise, ref;
ref = new Firebase('https://mysite.firebaseio.com/users/' + (user.provider + user.id) + '/registry/');
promise = angularFire(ref, $scope, 'devices');
});
It now will accurately create items under the user's id. However, still, once you logout and log back in, those items do not get cleared from $scope.devices. Therefore, they just add themselves to data but under the newly logged in user.
Update
I did a lot of trial and error. I probably set $scope.devices to [] and moved around login events in every possible combination. What eventually worked was #hiattp's fiddle in the accepted answer.
This is a result of the implicit data binding remaining intact as you switch users. If the new user shows up and creates a new binding, it will consider the existing data to be local changes that it should assimilate (that's why you see the original user's items being added to the new user), but if you try to clear them first without releasing the binding then you are implicitly telling Firebase to delete that data from the original user's item list (also not what you want). So you need to release the data bindings when you detect the logout (or login) events as needed.
The callback in the angularFire promise provides an "unbind" method (see here and here):
var promise = angularFire(ref, $scope, 'items');
promise.then(function(unbind){
// Calling unbind() will disassociate $scope.items from Firebase
// and generally it's useful to add unbind to the $scope for future use.
});
You have a few idiosyncrasies in your code that are likely causing it not to work, and remember that unbind won't clear the local collection for you. But just so you have an idea of how it should work (and to prove it does work) here is a fiddle.
You need to unbind $scope.items on logout. The best way to do this will be to save the unbind function given to your promise in $scope:
var ref = new Firebase('https://<my-firebase>.firebaseio.com/items/[user1id]');
angularFire(ref, $scope, 'items').then(function(unbind) {
$scope.unbindItems = unbind;
});
$scope.$on('angularFireAuth:logout', function() {
$scope.unbindItems();
});

Resources