In our project we are using Ractive together with Backbone.
Backbone.View has a "setElement" method, that basically sets the el property of a Backbone.View, thus allowing to attach the View to a different element of the DOM.
I was wondering if there is a similar functionality for a Ractive object.
Simply changing the el property of a Ractive object doesn't do the trick.
var oRactive = new Ractive(
{
"data": someData,
"el": someDomElement,
"template": someTemplate
});
// ... after doing some other stuff we'd like to set oRactive do a different el
// this.doesn't do the trick
oRactive.el = someOtherDomElement;
// this puts the renderedHTML in our DOM element but binding doesn't work
$(someOtherDomElement).html(oRactive.renderedHTML());
I'm not really surprised that the above doesn't work. Question is: Is there a way to make it work or is it generally impossible?
I am aware that I could just append oRactive.el to "someOtherDomElement" but that's not quite what I want.
This isn't something that Ractive currently supports, though it might in future. You could try doing the following:
frag = document.createDocumentFragment();
// move contents into the document fragment...
while ( oRactive.el.firstChild ) {
frag.appendChild( oRactive.el.firstChild );
}
// ...then into the DOM
someOtherDomElement.appendChild( frag );
// update oRactive.el so oRactive.find() still works
oRactive.el = someOtherDomElement;
Ractive stores references to individual nodes - in most cases it doesn't care about the actual shape of the DOM, and your situation shouldn't pose any obstacles (though I'd be interested to know if you run into any bugs doing this).
Related
TLDR
I like to really focus on keeping business logic away from the view model / controller. I find this sometimes rather hard in Meteor. Maybe I'm missing the point but I am after one of two things really:
1) A really good document explaining at a really low level how reactive values are being used.
2) A package that somehow manages an object so that if any of the setters are altered, they notify all of the get functions that would change as a result.
Unfortunately I've not seen either.
My Example
I have a fair bit ob business logic sitting behind a dialog used to document a consultation. I might have an event that sets a change of state.
I'd like to do something like this in the event:
const cc = new ConsultationEditor();
cc.setChiefComplaint(event.target.value);
console.log(cc.data());
ConsultationDict.set("consEdit", cc.data() );
When the user has updated this value, I'd then like to show a number of fields, based on the change. For this I have a helper with the following:
fields: function(){
console.log("trying to get fields");
const obj = ConsultationDict.get('consEdit');
cc = new ConsultationEditor(obj);
return cc.getFields();
}
But unfortunately this does not work for me.
What is your ConsultationDict?
The way you describe it, you want it to be a ReactiveDict as in the official ReactiveDict package.
https://atmospherejs.com/meteor/reactive-dict
Check this tutorial for examples:
https://themeteorchef.com/snippets/reactive-dict-reactive-vars-and-session-variables/
If you really need more fine tuning in your reactivity, you can also set a dependency tracker tracker = new Tracker.Dependency, and then refer to it wherever you change a variable with tracker.changed() and where the data needs to be notified with tracker.depend() like this:
var favoriteFood = "apples";
var favoriteFoodDep = new Tracker.Dependency;
var getFavoriteFood = function () {
favoriteFoodDep.depend();
return favoriteFood;
};
var setFavoriteFood = function (newValue) {
favoriteFood = newValue;
favoriteFoodDep.changed();
};
getFavoriteFood();
See the full Tracker doc here:
https://github.com/meteor/meteor/wiki/Tracker-Manual
I also found this gist to be useful to build reactive objects:
https://gist.github.com/richsilv/7d66269aab3552449a4c
and for a ViewModel type of behavior, check out
https://viewmodel.meteor.com/
I hope this helps.
From time to time I have the requirement to bind a control property to based on data out of model A to another model B.
For example the syntax could look like this (but will not work):
text : "{B>/rootB/{A>someValue}/propertyB}"
I normally solve this problem by "misusing" an unused control property in combination with the format function. It would look like this:
tooltip : {
path : "A>someValue",
formatter : function(oValue) {
// do some checks on oValue
var path = "B>/rootB/"+oValue+"/propertyB";
this.bindProperty("text", path);
return undefined; // because tooltip is not used
}
The benefit of this, each time "A>someValue" will be changed the binding of "text" will be updated automatically.
It is also possible to do this in template code (like items aggregations).
But you may smell the code ;)
Any suggestions to make it cleaner?
As far as I know, there is no such possibility in UI5 (yet). I always use a formatter function as you already mentioned. I say not YET, because developers seem to be aware of this feature request: see on GitHub
BUT, you dont need to missuse a random control property! Just use the formatter to read the needed values from any model you have access to:
text : {
path : "A>someValue1",
formatter : function(oValue) {
// read model B to get someValue2 (based on someValue1)
var path = "B>/rootB/"+oValue+"/propertyB";
var B = getModel("someModel");
var someValue2 = B.getProperty(path);
return someValue2
}
I've been messing around with CM conventions trying to understand how they work but i haven't found a decent article somewhere explaining step-by-step how and why.
However I've found a few code snippets that i've been working with with some success.
In this case, however, i don't understand what is going on.
I'm trying to bind a NumericUpDown Value and Maximum to a corresponding ViewModel property. I was able to do it with the following code:
Value
ConventionManager.AddElementConvention<NumericUpDown>(NumericUpDown.ValueProperty, "Value", "ValueChanged");
Maximum
ConventionManager.AddElementConvention<NumericUpDown>(NumericUpDown.MaximumProperty, "Maximum", "MaximumChanged");
var baseBindProperties = ViewModelBinder.BindProperties;
ViewModelBinder.BindProperties =
(frameWorkElements, viewModels) =>
{
foreach (var frameworkElement in frameWorkElements)
{
var propertyName = frameworkElement.Name + "Max";
var property = viewModels.GetPropertyCaseInsensitive(propertyName);
if (property != null)
{
var convention = ConventionManager.GetElementConvention(typeof(NumericUpDown));
ConventionManager.SetBindingWithoutBindingOverwrite(
viewModels,
propertyName,
property,
frameworkElement,
convention,
convention.GetBindableProperty(frameworkElement));
}
}
return baseBindProperties(frameWorkElements, viewModels);
};
However, an here comes the weird part, i can only make one of them to work. That makes me believe that i'm doing some noob mistake somewhere. It almost seems i can only call AddElementConvention and therefor only the last call is executed.
I would appreciate either a help with this piece of code or a reference to some good documentation that could help me with it.
Best Regards
i found out somewhere that CM only allows one convention per item so that's the reason of this behavior...
However since items like ComboBox allows binding for multiple properties (SelectedItem, ItemSource and so on...) i'm not completed convinced...
Why doesn't this work?
var spans = $();
var elem = document.getElementById('someId');
spans.add(elem);
What is the proper way to start off with an empty collection and add elements to it?
I want to loop through a collection of ids and find the element on the page and add it to the matched set.
Quoting from the jQuery website:
Given a jQuery object that represents a set of DOM elements, the .add() method constructs a new jQuery object from the union of those elements and the ones passed into the method.
Hence, when you do .add() it will not save the added elements, you would have to explicitly assign the element to the newly created object i.e
var $elements = $('.elements');
$elements = $elements.add($('#anotherelement'));
The .add() method returns a new jQuery object, so you'd need to overwrite the old one:
spans = spans.add( elem );
...or since you're adding DOM elements, you can modify the existing jQuery object with .push().
spans.push( elem );
EDIT: Changed .pushStack() to .push(), though I don't know if .push() is officially supported.
You could use .pushStack() to add a collection though.
spans = spans.pushStack( [elem] );
or
spans = spans.pushStack( document.getElementsByTagName('div') );
I guess I don't get what you're asking. If you have an element and you want to add it to a collection, you just use the .add() method just like you've already shown. What confuses some is that this returns a new jQuery object so you would do it like this:
var spans = $();
var elem = document.getElementById('someId');
spans = spans.add(elem);
Of course, something like this would be shorter:
var spans = $();
spans = spans.add('#someId');
And, of course, you don't have to start with an empty object. You would just start with:
var spans = $('#someId');
If you're looking to push or add items selected from a jQuery object, you could also do this:
var $els = $(),
$someEls = $(".some-els");
$els.push.apply($els, $someEls);
Just another way to do it.
What you actually want to do is use jQuery to it's full potential. You can use selectors to grab and create the collection right away. Your three lines above becomes one line:
var spans = $('#someId');
to add more ids to the collection, you can separate them with commas:
var spans = $('#someId1, #someid2, #someid3');
There may be better ways to do what you're trying, but if you just want to create an empty jQuery object, use this:
var $foo = $([]);
Edit: I should clarify - your code actually should work, unless you're using an older version of jQuery, in which case $() would create a collection containing the document object. In newer versions, however, there's nothing wrong with that. The code snippet I pasted above is just something that should work in older versions and newer versions of jQuery.
Edit 2: In response to this portion of the question: "I want to loop through a collection of ids and find the element on the page and add it to the matched set", the following code might be useful:
var ids = ['foo', 'bar', 'baz'],
selector = $.map(ids, function(i, id) {
return '#' + id;
}).join(','),
$collection = $(selector);
While this doesn't directly answer the question of "how to append to an existing jQuery selection", I have a work-around for this particular use-case.
You can pass an array of DOM elements to jQuery, and it will create a jQuery selection out of them.
var spansArray = [];
var elem = document.getElementById('someId');
spansArray.push(elem);
var spans = $(spansArray);
I can't think of any reason why you would need to add each element to the jQuery selection one-at-a-time, as opposed to all-at-once, so this should be a "drop-in-replacement" for your use case. In theory, this must also prove more efficient, as jQuery's .add() is ultimately just calling .push() on some array behind the scenes.
Try
var spans = $("<span />");
var elem = $("#someId").html();
spans.append(elem).appendTo('#someAnotherId');
instead
The reason your code doesn't work is because add does not change the collection, it returns a new jQuery object with the new elements in it. If you wanted, you could instead say spans = spans.add(elem);, but what you're doing is unnecessary: the beauty of jQuery is that it makes this sort of imperative programming unnecessary. Look at helloandre's answer for a much easier way to accomplish your goal.
It's like the following, if this makes sense:
var x = [1, 2, 3];
x.concat(4);
console.log(x); // [1, 2, 3] -- concat does not mutate the original array
I have some values stores in my model. I need to create a copy of those values, make some changes, and then output those changes without affecting the model values.
var my_source:Array = model.something.source
var output:Array = new Array();
for each (var vo:my_vo in my_source) {
if (vo.id == 1) {
vo.name = 'Foo';
output.push(vo);
}
else if (vo.id == 21) {
vo.name = 'Bar';
output.push(vo);
}
}
return output;
So, this works fine, except that any changes that are made when looping through my_source also seems to affect model.something. Why do changes to the my_source array affect the model? How do I prevent this from happening?
I've mentioned how to do this in my blog, but short answer is use ObjectUtil.copy(). What you're trying to do isn't copying since Flash uses reference based objects, so you're only copying the reference to the other array. By using ObjectUtil.copy(), you're doing what's called a 'deep copy' which is actually recreates the object in a new memory location.
You are dealing with references to data, not copies of data. This is how ActionScript-3 (and many other languages) works.
When you create the my_source variable, you are creating a reference to model.something.source, which also includes all of the references to your model objects. Further, when you loop through the my_vo objects, you are also getting a reference to these objects. This means that if you make changes to the object in this loop, you are making changes to the objects in the model.
How do you fix this? Inside your loop, you will need to make a copy of your object. I don't know what my_vo looks like, but if you have any other objects in that object tree, they would be references as well, which would probably require a "deep copy" to achieve what you want.
The easiest way (but usually not the most efficient way) to achieve a "deep copy" is to serialize and de-serialze. One way to achieve this:
function deepCopy(source:Object):* {
var serializer:ByteArray = new ByteArray();
serializer.writeObject(source);
serializer.position = 0;
return serializer.readObject();
}
Then, in your loop, you can make your copy of the data:
for each(var vo:my_vo in my_source) {
var copy:my_vo = deepCopy(vo);
// act on copy instead of vo
}