Polymer - How do I attach an observer to an array? - javascript

How do I attach an observer to a polymer attribute that is an array? To be clear, I want callbacks when items in the array change. For simplicity, let's say my array is:
[
{ text: 'foo' },
{ text: 'bar' }
]
I want something like:
observe : {
'items.text' : 'itemsChanged'
}
The following works, but is obviously un-sustainable:
observe : {
'items[0].text' : 'itemsChanged',
'items[1].text' : 'itemsChanged'
}
Note that in my case, the changes are coming from another polymer element that I have control over. So if I could somehow trigger a change from the element that has control over { text: 'foo' }, that would work as well.

To be clear, Polymer will automatically observe Arrays, but an 'ArrayObserver' will only tell you if (a) the Array itself is replaced or (b) items are added, removed, or rearranged. Just like observed objects, Polymer does not automatically observe properties of sub-objects.
the changes are coming from another polymer element that I have control over
This is usually the case and we typically have the element doing the changing fire an event for communication.

Kinda late, but because the question ranks quite high on google and none of the above is really helping, here is my solution:
Polymer ships with observe.js and this helps solving your problem.
The basic version would be to use the ArrayObserver but this only fires when the array gets altered, meaning when elements are added, removed and i guess also when you replace an entry. But this won't help if you wanna observe changes to object properties in an array - such as in your case.
Here it's better to use the CompoundObserver and add all paths via addPath or addObserver(new PathObserver(obj, '...')). But you have to iterate over the whole array and add all observed properties in a loop for instance.

You can use Object.observe() to accomplish this. e.g.:
ready: function() {
Object.observe(this.items, this.myObserver);
}
myObserver: function (changes){
changes.forEach(function(change, i){
console.log('what property changed? ' + change.name);
console.log('how did it change? ' + change.type);
console.log('whats the current value? ' + change.object[change.name]);
console.log(change); // all changes
});
},
More here.

There is a documentation for this
Observe array mutations

Related

How to get the text from an Insert event in CKEditor 5?

I am trying to process an insert event from the CKEditor 5.
editor.document.on("change", (eventInfo, type, data) => {
switch (type) {
case "insert":
console.log(type, data);
break;
}
});
When typing in the editor the call back is called. The data argument in the event callback looks like approximately like this:
{
range: {
start: {
root: { ... },
path: [0, 14]
},
end: {
root: { ... },
path: [0, 15]
}
}
}
I don't see a convenient way to figure out what text was actually inserted. I can call data.range.root.getNodeByPath(data.range.start.path); which seems to get me the text node that the text was inserted in. Should we then look at the text node's data field? Should we assume that the last item in the path is always an offset for the start and end of the range and use that to substring? I think the insert event is also fired for inserting non-text type things (e.g. element). How would we know that this is indeed a text type of an event?
Is there something I am missing, or is there just a different way to do this all together?
First, let me describe how you would do it currently (Jan 2018). Please, keep in mind that CKEditor 5 is now undergoing a big refactoring and things will change. At the end, I will describe how it will look like after we finish this refactoring. You may skip to the later part if you don't mind waiting some more time for the refactoring to come to an end.
EDIT: The 1.0.0-beta.1 was released on 15th of March, so you can jump to the "Since March 2018" section.
Until March 2018 (up to 1.0.0-alpha.2)
(If you need to learn more about some class API or an event, please check out the docs.)
Your best bet would be simply to iterate through the inserted range.
let data = '';
for ( const child of data.range.getItems() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
Note, that a TextProxy instance is always returned when you iterate through the range, even if the whole Text node is included in the range.
(You can read more about stringifying a range in CKEditor5 & Angular2 - Getting exact position of caret on click inside editor to grab data.)
Keep in mind, that InsertOperation may insert multiple nodes of a different kind. Mostly, these are just singular characters or elements, but more nodes can be provided. That's why there is no additional data.item or similar property in data. There could be data.items but those would just be same as Array.from( data.range.getItems() ).
Doing changes on Document#change
You haven't mentioned what you want to do with this information afterwards. Getting the range's content is easy, but if you'd like to somehow react to these changes and change the model, then you need to be careful. When the change event is fired, there might be already more changes enqueued. For example:
more changes can come at once from collaboration service,
a different feature might have already reacted to the same change and enqueued its changes which might make the model different.
If you know exactly what set of features you will use, you may just stick with what I proposed. Just remember that any change you do on the model should be done in a Document#enqueueChanges() block (otherwise, it won't be rendered).
If you would like to have this solution bulletproof, you probably would have to do this:
While iterating over data.range children, if you found a TextProxy, create a LiveRange spanning over that node.
Then, in a enqueueChanges() block, iterate through stored LiveRanges and through their children.
Do your logic for each found TextProxy instance.
Remember to destroy() all the LiveRanges afterwards.
As you can see this seems unnecessarily complicated. There are some drawbacks of providing an open and flexible framework, like CKE5, and having in mind all the edge cases is one of them. However it is true, that it could be simpler, that's why we started refactoring in the first place.
Since March 2018 (starting from 1.0.0-beta.1)
The big change coming in 1.0.0-beta.1 will be the introduction of the model.Differ class, revamped events structure and a new API for big part of the model.
First of all, Document#event:change will be fired after all enqueueChange blocks have finished. This means that you won't have to be worried whether another change won't mess up with the change that you are reacting to in your callback.
Also, engine.Document#registerPostFixer() method will be added and you will be able to use it to register callbacks. change event still will be available, but there will be slight differences between change event and registerPostFixer (we will cover them in a guide and docs).
Second, you will have access to a model.Differ instance, which will store a diff between the model state before the first change and the model state at the moment when you want to react to the changes. You will iterate through all diff items and check what exactly and where has changed.
Other than that, a lot of other changes will be conducted in the refactoring and below code snippet will also reflect them. So, in the new world, it will look like this:
editor.document.registerPostFixer( writer => {
const changes = editor.document.differ.getChanges();
for ( const entry of changes ) {
if ( entry.type == 'insert' && entry.name == '$text' ) {
// Use `writer` to do your logic here.
// `entry` also contains `length` and `position` properties.
}
}
} );
In terms of code, it might be a bit more of it than in the first snippet, but:
The first snippet was incomplete.
There are a lot fewer edge cases to think about in the new approach.
The new approach is easier to grasp - you have all the changes available after they are all done, instead of reacting to a change when other changes are queued and may mess up with the model.
The writer is an object that will be used to do changes on the model (instead of Document#batch API). It will have methods like insertText(), insertElement(), remove(), etc.
You can check model.Differ API and tests already as they are already available on master branch. (The internal code will change, but API will stay as it is.)
#Szymon Cofalik's answer went into a direction "How to apply some changes based on a change listener". This made it far more complex than what's needed to get the text from the Document#change event, which boils down to the following snippet:
let data = '';
for ( const child of data.range.getChildren() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
However, reacting to a change is a tricky task and, therefore, make sure to read Szymon's insightful answer if you plan to do so.

Polymer, send variable i am setting into another element

I am trying to toggle different events by a string which I am setting between 2 animated pages
I have some code coming back up onto the page holding both animated pages that swaps and sets the id I want to send like so
_onElementClick: function(event) {
this.$.dataId = event.detail.data.id;
this.$.pages.selected = 1;
},
so the pages.selected changes the page perfectly fine, and if I log this.$.dataId it is the correct value, howeber on this page i am trying to pass this.$.dataId into another element like so
<my-polymer-full dataId="dataId"></my-polymer-full>
and use it in the element like
<div>[[dataId]]</div>
and in the element I have it set like
properties: {
dataId: {
type: String
},
and this does not seem to work. I am new to polymer and unsure what I am doing incorrectly.
You shouldn't be using node selectors, this.$, when trying to use properties. Instead, consider setting property values on the element itself like:
_onElementClick: function(event) {
this.dataId = event.detail.data.id;
this.$.pages.selected = 1;
}
Then use one or two-way data-binding to bind those properties to the properties of the elements you want to use them in like:
<my-polymer-full data-id="[[dataId]]"></my-polymer-full>
Importantly, note the use of data-id. This is how you need to represent camelCase properties published by any custom Polymer element. That was probably your main issue originally.

JavaScript watch event not working on DOM element objects?

I know I can use watch to bind a callback that will be triggered when object property changes. And this does work on generic objects like:
{'a':1, 'b':'7'}
So, I thought that I can simply do this to bind a callback that will trigger when input field value changes:
var inputDomElement = document.getElementById('someInputElement');
inputDomElement.watch('value',function (id, oldval, newval) {
alert(oldval);
alert(newval);
});
But this doesn't work. Simply doesn't trigger. No alert boxes. I've tried it in Firefox 5 and Google Chrome (latest).
Is this not how watch works? Is watch simply doesn't work on DOM elements? I thought that they're simply objects - aren't they?
UPDATE 1:
Here's MDN info about what watch is:
https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/watch
UPDATE 2:
I cannot use change event. because change only triggers when text element catches blur. Meaning that it'll only trigger when user switches from this textfield to another one. It's not in any way dynamic for when for example checking if this username or email address already taken which I'd like to happen on each distinct change.
The DOM is written in C/C++ where the concept of getting and setting a Javascript variable doesn't exist as you or I would often imagine it. You probably imagined the code to be implemented similar to what is below. Unfortunately Object.watch is never initiated because the DOM isn't constantly updating the Javascipt value, but Javascript is requesting an update from the DOM.
input.onuserchangevalue = function(){
input.value = 'new user input'
}
Thinking how the DOM commonly works, each element has dozens of potential properties.
innerHTML,value,style.cssText,name,id,style.background,style.backgroundColor
Imagine if the DOM underlining code had to constantly update every DOM elements Javascript properties %) Memory and CPU cycles would go through the roof having to ensure the properties matched the display value. The DOM's internals would also have to check if the Javascript value has potentially changed.
Reality - Red Pill
Basically the DOM isn't giving info to javascript, but Javascript is requesting the info from the DOM. This is a decent Javascript interpretation of what is going on underneath.
Object.defineProperty(input, "value", {
get : function(){ /* get C/C++ DOM value */ },
set : function(){ /* change C/C++ DOM value */ }
});
This explains why the DOM is often the bottleneck for Javascript. Javascript has to request/set the internal DOM values every time you interface with the DOM.
You need to use jQuery Objects, not DOM Objects. There is a difference.
document.getElementById("someID") //returns DOM Object
$('#someId') //returns jQuery Object
Note: you could doe something strange like this:
$(document.getElementById("someID")) //returns a jQuery Object
this would work
$('#someInputElement').watch('value',function (id, oldval, newval) {
alert(oldval);
alert(newval);
});
if you want to track changes on a text element, why not just use the .change() method?
http://jsfiddle.net/rkw79/qTTsH/
$('input').change(function(e) {
$('div').html('old value: ' + e.target.defaultValue + '<br/>'
+ 'new value: ' + e.target.value);
})
For instant change, use .keyup(): http://jsfiddle.net/rkw79/qTTsH/1/

Why do they use references instead of jQuery objects?

In this tutorial, they use containerElement.rating and starElement.rating for storing rating.
The question is : Why?? is it possible to use jQuery objects container and star like
container.rating and star.rating??
In their example, How does it work:
star.click(function() {
containerElement.rating = this.rating;//What does 'this' refer to?? Star or starElement??
elements.triggerHandler("ratingchanged", {rating: this.rating});
});
It's doable/allowable, but not recommended. Nothing says that jquery couldn't suddenly decide to add a .star attribute at some point in the future.
If you need to attach your own data to a dom element, then use someelement.data(key, val) instead, which adds your data in a method guaranteed to not conflict with any future changes to the DOM specs.
It is possible to store objects like you're saying, but I believe this tutorial creates some circular references (JS->DOM->JS) which is bad. They should be using jQuery's .data() function to avoid circular references, which cause memory leaks.

Rendering suggested values from an ext Combobox to an element in the DOM

I have an ext combobox which uses a store to suggest values to a user as they type.
An example of which can be found here: combobox example
Is there a way of making it so the suggested text list is rendered to an element in the DOM. Please note I do not mean the "applyTo" config option, as this would render the whole control, including the textbox to the DOM element.
You can use plugin for this, since you can call or even override private methods from within the plugin:
var suggested_text_plugin = {
init: function(o) {
o.onTypeAhead = function() {
// Original code from the sources goes here:
if(this.store.getCount() > 0){
var r = this.store.getAt(0);
var newValue = r.data[this.displayField];
var len = newValue.length;
var selStart = this.getRawValue().length;
if(selStart != len){
this.setRawValue(newValue);
this.selectText(selStart, newValue.length);
}
}
// Your code to display newValue in DOM
......myDom.getEl().update(newValue);
};
}
};
// in combobox code:
var cb = new Ext.form.ComboBox({
....
plugins: suggested_text_plugin,
....
});
I think it's even possible to create a whole chain of methods, calling original method before or after yours, but I haven't tried this yet.
Also, please don't push me hard for using non-standard plugin definition and invocation methodics (undocumented). It's just my way of seeing things.
EDIT:
I think the method chain could be implemented something like that (untested):
....
o.origTypeAhead = new Function(this.onTypeAhead.toSource());
// or just
o.origTypeAhead = this.onTypeAhead;
....
o.onTypeAhead = function() {
// Call original
this.origTypeAhead();
// Display value into your DOM element
...myDom....
};
#qui
Another thing to consider is that initList is not part of the API. That method could disappear or the behavior could change significantly in future releases of Ext. If you never plan on upgrading, then you don't need to worry.
So clarify, you want the selected text to render somewhere besides directly below the text input. Correct?
ComboBox is just a composite of Ext.DataView, a text input, and an optional trigger button. There isn't an official option for what you want and hacking it to make it do what you want would be really painful. So, the easiest course of action (other than finding and using some other library with a component that does exactly what you want) is to build your own with the components above:
Create a text box. You can use an Ext.form.TextField if you want, and observe the keyup event.
Create a DataView bound to your store, rendering to whatever DOM element you want. Depending on what you want, listen to the 'selectionchange' event and take whatever action you need to in response to the selection. e.g., setValue on an Ext.form.Hidden (or plain HTML input type="hidden" element).
In your keyup event listener, call the store's filter method (see doc), passing the field name and the value from the text field. e.g., store.filter('name',new RegEx(value+'.*'))
It's a little more work, but it's a lot shorter than writing your own component from scratch or hacking the ComboBox to behave like you want.
#Thevs
I think you were on the right track.
What I did was override the initList method of Combobox.
Ext.override(Ext.form.ComboBox, {
initList : function(){
If you look at the code you can see the bit where it renders the list of suggestions to a dataview. So just set the apply to the dom element you want:
this.view = new Ext.DataView({
//applyTo: this.innerList,
applyTo: "contentbox",
#qui
Ok. I thought you want an extra DOM field (in addition to existing combo field).
But your solution would override a method in the ComboBox class, isn't it? That would lead to all your combo-boxes would render to the same DOM. Using a plugin would override only one particular instance.

Categories

Resources