Path notification for changes to an object within an array? - polymer-1.0

We have a large model object that is owned and managed outside of polymer. We want to expose this model to polymer elements through a proxy element that exposes computed parts of it.
For example, the model may have:
{
blocks: {
...
properties: [
{ ... }, // prop0
{ ... }, // prop1
]
},
}
We are using recursive object observers and array observers to monitor changes to the model and notify polymer appropriately (either using .notifyPath(path, ...) for object changes and ._notifySplice(...) for array changes). However, there doesn't seem to be a good way for us to notify of changes to an object within an array, e.g. prop0 changed in the example above.
Is there? What should the path be?

Here's an example using binding to show an updated Polymer property nested inside an object and array:
code:
<div>
<h1>[[blocks.properties[0]]]</h1>
<p>this updated property should read: Apple</p>
</div>
script:
Polymer({
is: "my-page",
properties: {
blocks: {
type: Object,
value: { properties : ["acorn","banana","carrot"] },
notify: true
},
},
attached: function() {
console.log("update");
this.set('blocks.properties[0]', "Apple");
},
If you need to do array functions you should also use this.push(path, value) or this.splice(path, value) to notify changes.

Related

Derive locally cached Apollo client state based off query/cached object updates

I have a query that retrieves a Model. Inside this model, there are nested models with fields.
The shape is roughly like this:
{
model: [
{
id: 1,
fields: [...]
},
{
id: 2,
fields: [...]
}
]
}
Additionally, the frontend needs the model normalized into a list, like this:
{
modelFields: [
{...},
{...},
{...},
{...}
]
}
I’m attempting to derive modelFields declaratively when a query or cache update changes model. I’m trying to achieve this in type-policies section on Model: { merge: modelMergeMiddleware }, like so:
export function modelMergeMiddleware(
__: ModelFragment,
incoming: ModelFragment,
{cache, readField}: FieldFunctionOptions
) {
if (incoming) {
cache.writeQuery({
query: ModelFieldsDocument,
data: {
modelFields: incoming.fieldsets.reduce(
(fields: ModelFieldFragment[], fieldset: FieldsetFragment) => {
return fields.concat(newFields)
},
[]
)
}
})
}
return incoming
}
However, this runs into problems:
nested cache references don’t get passed through leaving empty data
readField and lodash’s _.cloneDeep both result in Readonly data that cause errors
My question is two-fold:
Is there a method to work around the problems mentioned above to derive data in a merge function?
Is there a different approach where I can declaratively derive local-only state and keep it synchronized with cached objects?
Per question 2, my backup approach is to use a reactiveVar/Recoil to store this data. This approach has the tradeoff of needing to call a setter function in all the places the Model object gets queried or mutated in the app. Not the end of the world, but it’s easy to miss a spot or forget about the setter function.

Avoid serialization of properties

When I require to add a private property to an object (for view or logic control) that will be submitted to a rest api latter, is valid prefix the property with $$? This is tricky in cases when I have an object with a list of children and each child requires a private property that should not be sent.
{
name: 'my object',
items: [
{
name: 'my child',
$$editing: true
},
{
name: 'my other child',
$$editing: true
}
]
}
Yes, angularjs $http service uses the angular.toJson method by default.
All properties with $$ are filtered out, because angular uses such properties internally. (e.g. you may have seen the $$hashKey property, which is added by angular)
You can try:
console.log(angular.toJson({a:1, $$b:2, c: {x:2,$$_y:3}}))
results in "{"a":1,"c":{"x":2}}"

BackboneJs - Retain events on a collection inside a model when model changes

i've a Collection inside a Model as illustrated below:
var itemModel = Backbone.Model.extend({
defaults:{
name:"",
brand:"",
priceCollection:[]
}
})
There are change listeners attached to the itemModel and also change listeners attached to collection as
this.listenTo(itemModel.get('priceCollection'),'change',this.dosomething) in a view.
The problem is that the change listeners on the collection work fine as long as the parent model hasn't changed , if the model is given a set of new attributes via itemModel.set(newattributes) the event bound on itemModel.get('priceCollection') is lost.
How do i retain this event? or should i rebind this event everytime the Model is change? or Should i move the listener on the collection from the view to the Model and trigger a custom Backbone event?
It should be noted that this model is singleton
Keep in mind that Backbone assumes a Collection and Model should be mapped 1:1 to a server side resource. It makes clear assumtions on the API layout and data structures - refer to Model.url, Model.urlRoot and Collection.url.
Proposal
You said the model is a singleton. In this case I'd suggest to maintain the model and collection separately.
Since a SomeModel is not accompanied by a certain collection SomeCollection which have a tight relationship it's not necessary to relate them on an attribute level. The effort needed to establish event listeners and sync the data is only at one place.
// some controller (app main)
var model = new SomeSingletonModel();
var collection = new SomeSingletonCollection();
var view = new SomeView({
model: model,
collection: collection
});
Probably the resource that is mapped to SomeSingletonModel will deliver an array.
What are the benefits of using a collection as model attribute (that's what model.get("name") is) over using a plain array? Syncing and change events. Both are probably only necessary when a View updates the Collection's Models. When the View only renders, a Collection does not provide any benefit in many cases.
If the data of that array needs to be updated, using a Collection is probably the right choice because of Backbone's synching mechanisms.
But how to keep the collection sync with the model (you ask)?
Your controller needs to listen to the model and update the collection on sync and reset:
model.on("sync reset", function() {
// "priceCollection" is a model attribute
collection.reset(model.get("priceCollection"));
// optionally unset "priceCollection" on the model
this.unset("priceCollection", { silent: true });
});
This will initialize the collection.
Any change to the Collection's Models will then only be part of the Collection's or Model's syncing mechanisms.
Foreword
Also see my other answer which is probably the better choice when the model is a singleton.
Note the very first statement on Backbone's assumptions on the API design on that answer.
Proposals using a coupling between Model and Collection
Note: if necessary, in all these implementations the Collection's url or Model's (the Collection's Model) url/rootUrl may get (re-)defined upon sync to control the syncing.
Update internal reference on change/sync/reset
This implementation removes the model attribute and updates an object attribute with its data.
The object attribute is one Collection instance that is only reset, not recreated, upon model change.
var CustomModel = Backbone.Model.extend({
defaults: {
// defaults go here - "children" may be contained here
},
// implement constructor to act before the parent constructor is able to
// call set() (L402 in v1.3.0) with the initial values
// See https://github.com/jashkenas/backbone/blob/1.3.0/backbone.js#L402
constructor: function() {
// create children collection as object attribute - replaces model attr.
this.children = new Backbone.Collection();
// listen to changing events to catch away that attribute an update the
// object attribute
this.listenTo(this, "change:children sync reset", this.onChangeColl);
// apply original constructor
Backbone.Model.apply(this, arguments);
},
onChangeColl: function() {
// check for presence since syncing will trigger "set" and then "sync",
// the latter would then empty the collection again after it has been updated
if (this.has("children")) {
// update "children" on syncing/resetting - this will trigger "reset"
this.children.reset(this.get("children"));
// remove implicitly created model attribute
// use silent to prevent endless loop due to change upon change event
this.unset("children", { silent: true });
}
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomModel({ a: 1, children: [{ x: 1 }, { x: 5 }] });
c.set({a: 8, children: [{ x: 50 }, { x: 89 }]});
c.url = "/dummy"
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, children: [{ x: 42 }, { x: 47 }] }); } }
c.fetch();
Pro
listening to collection events is easier to implement since there's one instance through model lifetime
Contra
code is more complex
collection data is not part syncing without further implementations
Replace on change/sync/reset
This implementation intercepts model attribute changes and replaces its data with a Collection instance that has been initialized (reset) with the raw data.
var CustomModel = Backbone.Model.extend({
defaults: {
// this is optional
children: new Backbone.Collection()
},
initialize: function() {
// listen to model attribute changing events to swap the raw data with a
// collection instance
this.listenTo(this, "change:children sync reset", this.onChangeColl);
},
onChangeColl: function() {
if (this.has("children")) {
// use silent to prevent endless loop due to change upon change event
this.set("children", new Backbone.Collection(this.get("children")), { silent: true });
}
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomModel({ a: 1, children: [{ x: 1 }, { x: 5 }] });
c.set({ a: 8, children: [{ x: 50 }, { x: 89 }] });
c.url = "/dummy";
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, children: [{ x: 42 }, { x: 47 }] }); } }
c.fetch();
Pro
straightforward implementation
Contra
data included in sync, excluding it takes more effort
listening to the Collection impractical: since all consumers would need to unbind/bind
Note: depending on your requirements and API design you may not want children being synced to the server automatically. In this case this solution is limited. You could overwrite toJSON() of the Model but this may limit its usage for other parts of the application (like feeding the data into a view).
Inverse relation: Collection has a Model
Maybe your primary data is actually the Collection. So decorating a Collection with additional data is another approach. This implementation provides one model along the Collection that will be updated upon collection sync.
This implementation is only best suited for fetch of collection data along with attributes (e.g. fetching directory contents with attributes of the directory itself).
var CustomCollection = Backbone.Collection.extend({
initialize: function() {
// maintain decorative attributes of this collection
this.attrs = new Backbone.Model();
},
parse: function(data, opts) {
// remove "children" before setting the remainder to the Model
this.attrs.set(_.omit(data, "children"));
// return the collection content only
return data.children;
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomCollection({ a: 1, b: 2, children: [{ x: 2 }, { x: 3 }] }, { parse: true });
c.reset({ a: 9, b: 11, children: [{ x: 5 }, { x: 10 }] } , { parse: true });
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, b: 124, children: [{ x: 42 }, { x: 47 }] }) } }
c.fetch();
Pro
straight forward implementation
delegating model events is easier
Contra
collection data is not part syncing without further implementations
requires parse() to be implemented which
-- in turn requires parse: true to always be passed to reset() and set() and
-- requires parse() to be called with the collection as scope (this) (this could be circumvented by defining parse within initialize bound to this using `bind()´)

Extending UI-Router-Tabs To Display Form Validation State

I'm pretty new to Angular and having a problem figuring out how to bind a value from a data service to a customization of the https://github.com/rpocklin/ui-router-tabs project that I have created.
The customization is to display an icon in the tab heading that toggles based on the validation state of the corresponding ui-view. I've wired up a data service that is updated when validation succeeds or fails, and the binding is being "seen" by the controller that renders the tabs.
The problem arises when I attempt to pass this value into the tab data as "options". The value change/icon toggle is not being processed as the validation state changes. If I pass a hard-coded "true" into the tab data, the desired icon is displayed.
vm.tabs = [
{
heading: "View 1",
route: "manage.view1",
options: {
status: vm.stateService.getForm1State()
}
},
{
heading: "View 2",
disable: vm.disableTabs,
route: "manage.view2",
options:{
status: true
}
}
];
This doesn't seem like something that should be that difficult, so I think I'm just missing something obvious about the scoping. Here is a plunk http://plnkr.co/edit/iefvwcffSZmpfy83NGde?p=preview that demonstrates the issue.
Note: should be tagged as ui-router-tabs, but I lack the reputation to create the tag.
The problem lies here:
options: {
status: vm.stateService.getForm1State()
}
Since stateService.getForm1State() returns a boolean value, this value will be copied and assigned as the value of the status property.
So when this code has executed once it will for example be:
options: {
status: false
}
The options object won't react to any changes within the stateService.
An easy solution is to have stateService.getForm1State() return an object instead.
For example:
function stateService() {
var form1IsValid = {
status: false
};
return {
getForm1State: getForm1State,
setForm1State: setForm1State
};
function getForm1State() {
return form1IsValid;
}
function setForm1State(newValue) {
form1IsValid.status = newValue;
return;
}
Then make the options object refer to the same object:
options: vm.stateService.getForm1State()
Now whenever you have:
options.status
It will be the same as:
vm.stateService.getForm1State().status
And it will correctly reflect the changes.
Demo: http://plnkr.co/edit/KjXVSSMJbLw7uod0ac6O?p=preview

Access selectedItems of an <iron-list> on change

Having trouble understanding how to access the selectedItems property of an iron-list. When bound to a property of the parent, I can output it's contents using a template, but an observer or computed binding does not fire when it changes and I can't seem to output it's contents.
If anyone could point out what I'm doing wrong, how I can read the selectedItems for logic, or how I can observe changes to an iron-list selectedItems property, I would greatly appreciate the know how. Thanks.
Below is some example code to show how I am using the selectedItems property of iron-list:
<dom-module id="example-component">
<template>
<iron-list
id="dataList"
items="[[data]]"
as="item"
multi-selection
selection-enabled
selected-items="{{selectedItems}}">
<template>
<span>[[item.data]]</span>
</template>
</iron-list>
<template is="dom-repeat" items="[[selectedItems]]" index-as="index">
<div>[[item.data]]</div> <!-- this displays any selected item -->
</template>
</template>
<script>
Polymer({
is: 'example-component',
properties: {
data: {
type: Array,
value: [
{
'data': "item 0"
},
{
'data': "item 1"
}
]
},
selectedItems:
{
type: Object,
observer: '_selectedItemsChanged'
}
},
_selectedItemsChanged: function(){
console.log(this.selectedItems); //this neither runs nor outputs anything when an item is selected
}
});
</script>
From the docs:
Finally, to observe mutations to arrays (changes resulting from calls to push, pop, shift, unshift, and splice, generally referred to as “splices”), specify a path to an array followed by .splices as an argument to the observer function.
Therefore, remove the selectedItems property and add a manual observer like so:
observers: [
'_selectedItemsChanged(selectedItems.splices)'
],

Resources