After Commit 48 (Beta Candidate) i can't get observable array logic anymore. I know it has changed. I've read the changelog and been playing with new commit for some time but couldn't get it working. Helpers just don't update anymore. Any help appreciated.
Here is a simple example. Clicking "add friend" should call friends_names again.. but it doesn't anymore:
<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery.js"></script>
<script src="http://www.jsviews.com/download/jsviews.js"></script>
</head>
<body>
<div id="people"></div>
<script id="peopleTemplate" type="text/x-jsrender">
<button id="add">Add person</button><br />
{^{for people}}
<div>
Name: {{>name}},
Friends: <span data-link="html{:~friends_names(#data.friends)}"></span>
<button class="friend-add">add friend</button>
</div>
{{/for}}
</script>
<script>
var data = {
people: [
{
name: "Adams",
friends: [
{name:'Petere'},
{name:'Steve'}
]
},
{
name: "Eugenia",
friends: [
{name:'Bob'}
]
}
]
};
$.templates({
peopleTmpl: "#peopleTemplate"
});
var friends_names = function(friends){
friends = friends || []
var names = []
for (var i=0, l=friends.length; i<l; i++) {
names.push(friends[i].name);
}
return '<b>' + names.join(', ') + '</b>';
};
$.views.helpers({friends_names:friends_names});
$.templates.peopleTmpl.link("#people", data);
//debug
$.observable(data).observeAll(function (ev, obj) { console.log('change', obj); });
$("#add").on("click", function() {
$.observable(data.people).insert({
name: "Amos",
friends: []
});
})
$('#people').on('click', '.friend-add', function(e){
e.preventDefault();
var name = 'Some anonymous friend' + Math.floor((Math.random()*100)+1);
var friends = $.view(this).data.friends;
$.observable(friends).insert({
name: name
});
});
</script>
</body>
</html>
I know nested template can be used (not sure if it will solve the problem) but in real application there is much more logic in helper, thus nested template won't help.
Yes, this is deliberate: See the commit note:
Data linking to arrays is simplified and more consistent. Now tags DO NOT automatically bind to arrays, and refresh when the array
updates. {^{myTag path.to.array/}} will now update when the to.array
property is update (property change) but not when the to.array
itself changes observably. (array change). A tag should opt in to
arraybinding either by deriving from the "for" tag - as in the
'range' sample: http://www.jsviews.com/#samples/tag-controls/range,
or by following the using onAfterLink and onDispose to add/remove
the onArrayChange handler, as in the {^{myWidget .../}} sample in
the JsViews unit tests. This change relates to
https://github.com/BorisMoore/jsviews/issues/158
Here is a really simple fix. If you include the array.length as a parameter (even if your helper function doesn't use it) then JsViews will respond to changes in the array length (which is a property change, not an array change) and will trigger a refresh for your helper: ~friends_names(friends, friends.length)
{^{for people}}
<div>
Name: {{>name}},
Friends: <span data-link="html{:~friends_names(friends, friends.length)}"></span>
<button class="friend-add">add friend</button>
</div>
{{/for}}
Related
I'm trying to determine the best way to establish cross-component-communication. My first thought was to use ractive.fire with a wildcard, but that doesn't seem to work. Am I trying to mis-use ractive.fire? What would be the suggested way for doing cross-component-communication with ractive?
Ractive.components.pubSub = Ractive.extend({
oninit() {
this.on('*.customEvent', () => alert('pub sub got your custom event!'))
}
})
Ractive.components.something = Ractive.extend({
template: '#something'
})
let ractive = new Ractive({
target: 'body',
template: '#app'
})
<script src="https://cdn.jsdelivr.net/npm/ractive#0.10.3/ractive.js"></script>
<script id="app" type="text/ractive">
<pubSub />
<something />
</script>
<script id="something" type="text/ractive">
<button on-click="#.fire('customEvent')">Fire Custom Event</button>
</script>
Ractive doesn't prescribe a convention for data sharing/cross-component communication. However, it does give you the facilities to do it. A common practice I've seen is to create a "dummy instance" and use its ractive.fire() and ractive.on() methods.
// The dummy instance, make it visible to components.
const pubsub = Ractive()
const SourceComponent = Ractive.extend({
template: '<button click="send()">Click me</button>',
send(){
pubsub.fire('message')
}
})
const ListeningComponent = Ractive.extend({
onInit(){
pubsub.on('message', () => {
console.log('called')
})
}
})
Alternatively, if all you want is to share state across all components, modify them anywhere you want, and have everyone re-render on change, you can put that state in #shared.
I'm trying to implement sort and search to my items, so i started with sort and it works:
Template
<button class="sort">Sort</button>
{{#each cvs}}
{{> Interviu}}
{{/each}}
JS:
Template.Interviuri.onCreated(function () {
var self = this
self.autorun(function () {
self.sortOrder = new ReactiveVar(-1)
})
Template.Interviuri.helpers({
cvs() {
const instance = Template.instance()
return Cvs.find({}, { sort: { createdAt: instance.sortOrder.get() } })
},
})
Template.Interviuri.events({
'click .sort'(event, instance) {
instance.sortOrder.set(instance.sortOrder.get() * -1)
Next i wanted to implement Search on the same page. So the best way i could found was EasySearch.
But using EasySearch, it means i must change the way my items are being displayed. And then the sort doesn't work anymore.
Template
<div class="searchBox pull-right">
{{> EasySearch.Input index=cvsIndex attributes=searchAttributes }}
</div>
{{#EasySearch.Each index=cvsIndex }}
{{> Interviu}}
{{/EasySearch.Each}}
Collection
CvsIndex = new EasySearch.Index({
collection: Cvs,
fields: ['name'],
engine: new EasySearch.Minimongo()
})
JS
cvsIndex: () => CvsIndex,
How can i have both search and sort working at the same time?
With EasySearch you can use two methods on your index, namely getComponentDict() and getComponentMethods().
With getComponentDict() you can access search definition and options:
index.getComponentDict().get('searchDefinition');
index.getComponentDict().get('searchOptions');
You also have the corresponding setters to change the search definition/option.
getComponentMethods has mehods like
index.getComponentMethods().loadMore(integer);
index.getComponentMethods().hasMoreDocuments();
index.getComponentMethods().addProps(prop, value);
index.getComponentMethods().removeProps([prop])
From that you can set your prop, say index.getComponentMethods().addProp('sort', -1) and then on the index definition, in your MongoDB engine, set the sort from that prop:
index = new EasySearch.index({
// other parameters
engine: new EasySearch.MongoDB({
sort: function(searchObject, options) {
if(options.search.props.sort) {
return parseInt(options.search.props.sort);
}
return 1;
}
})
});
See EasySearch Engines for more info.
I create these two local collections (the code is actually written one after the other exactly like below):
ShoppingCartCollection = new Meteor.Collection(null);
CurrentPricesCollection = new Meteor.Collection(null);
Inside Template.myTemplate.rendered I add some initial info into these collections (again, code is one after the other):
ShoppingCartCollection.insert({"sqft" : "not yet entered"});
CurrentPricesCollection.insert({"hdrPhotos" : 100});
I've got these two global helpers in helpers.js (defined one after the other)
Handlebars.registerHelper("shoppingCart", function() {
return ShoppingCartCollection.findOne();
});
Handlebars.registerHelper("currentPrice", function() {
return CurrentPricesCollection.findOne();
});
When I load the page I immediately run these commands in the console:
> ShoppingCartCollection.findOne();
Object {sqft: "not yet entered", _id: "xcNmqJvMqqD5j7wwn"}
> CurrentPricesCollection.findOne();
Object {hdrPhotos: 100, _id: "LP38E3MZgzuYjvSec"}
In my template I use these helpers, but...
{{currentPrice.hdrPhotos}} //displays nothing
{{shoppingCart.sqft}} //displays "not yet entered"
How... what... ? How can this be? Are there some kind of gotchas that I could be missing? Some kind of dependency or load order that I'm not aware of?
The code you posted is working fine here.
Suggest comparing this code to the exact details of what you are doing. Also, look
for other problems, typos, etc.
Below is the exact test procedure I used:
From nothing, at the linux console:
meteor create sodebug
Note that this will produce files for a "hello world" type program.
Check the version:
meteor --version
Release 0.8.1.1
Edit sodebug/sodebug.js:
if (Meteor.isClient) {
// code autogenerated by meteor create
Template.hello.greeting = function () {
return "Welcome to sodebug.";
};
Template.hello.events({
'click input': function () {
// template data, if any, is available in 'this'
if (typeof console !== 'undefined')
console.log("You pressed the button");
}
});
// add your code here
ShoppingCartCollection = new Meteor.Collection(null);
CurrentPricesCollection = new Meteor.Collection(null);
ShoppingCartCollection.insert({"sqft" : "not yet entered"});
CurrentPricesCollection.insert({"hdrPhotos" : 100});
Handlebars.registerHelper("shoppingCart", function() {
return ShoppingCartCollection.findOne();
});
Handlebars.registerHelper("currentPrice", function() {
return CurrentPricesCollection.findOne();
});
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
});
}
Edit sodebug.html:
<head>
<title>sodebug</title>
</head>
<body>
{{> hello}}
{{> T1 }}
{{> T2 }}
</body>
<template name="T1">
<p>
{{shoppingCart.sqft}}
</p>
</template>
<template name="T2">
<p>
{{currentPrice.hdrPhotos}}
</p>
</template>
<template name="hello">
<h1>Hello World!</h1>
{{greeting}}
<input type="button" value="Click" />
</template>
Run: meteor run
Manual tests:
Fire up chromium browser at localhost:3000
Check web browser console for collections data. PASS
Check web browser screen for templates data. PASS
Reorder templates in sodebug.html file, check web browser screen. PASS
I tried to play with the leaderboard example by adding a button to toggle sorting by name/score.
I succeed in adding event, but I also want to change the text value on the button.
I expect the added button text value(sort by name/score) get updated reactively(every time I click the button), however it fails to do so, only get updated when manually click on the player item.
Code added:
leaderboard.html(leaderboard template):
<template name="leaderboard">
<div class="leaderboard">
<input type="button" name="sort" value="Sort by {{sort_type}}" class='sort'/>
{{#each players}}
{{> player}}
{{/each}}
</div>
{{#if selected_name}}
<div class="details">
<div class="name">{{selected_name}}</div>
<input type="button" class="inc" value="Give 5 points" />
</div>
{{else}}
<div class="none">Click a player to select</div>
{{/if}}
</template>
leaderboard.js(client part):
if (Meteor.isClient) {
Meteor.startup(function(){
Session.setDefault('sort_order', {score: -1, name: 1});
Session.setDefault('sort', 'name');
});
Template.leaderboard.players = function () {
return Players.find({}, {sort: Session.get('sort_order')});
};
Template.leaderboard.selected_name = function () {
var player = Players.findOne(Session.get("selected_player"));
return player && player.name;
};
Template.leaderboard.sort_type = function() {
return Session.get('sort');
};
Template.player.selected = function () {
return Session.equals("selected_player", this._id) ? "selected" : '';
};
Template.leaderboard.events({
'click input.inc': function () {
Players.update(Session.get("selected_player"), {$inc: {score: 5}});
},
'click input.sort': function(){
var sort_order = Session.get('sort_order');
if (_.isEqual(sort_order, {score: -1, name: 1}) || _.isEqual(sort_order, {score: 1})) {
Session.set('sort_order', {name: 1});
Session.set('sort', 'score');
} else if (_.isEqual(sort_order, {name: 1})){
Session.set('sort_order', {score: 1});
Session.set('sort', 'name');
}
}
});
Template.player.events({
'click': function () {
Session.set("selected_player", this._id);
}
});
}
Thanks!
B.R.
Ian
This has to do with the preserve-inputs package. From the Meteor Docs:
By default, new Meteor apps automatically include the preserve-inputs package. This preserves all elements of type input, textarea, button, select, and option that have unique id attributes or that have name attributes that are unique within an enclosing element with an id attribute. To turn off this default behavior, simply remove the preserve-inputs package.
So basically because your button has a name attribute, Meteor is preventing it from changing when the template is re-rendered. There are three ways you can solve this:
Simply remove the name attribute from your input.sort attribute.
Remove the preserve-inputs package (not recommended if you're using the current template engine).
You can use the preview release of the new Meteor template engine, which no longer needs the preserve-inputs package because it automatically does more fine-grained DOM updates. You can run the app once with the preview release using:
meteor --release shark-1-29-2014-e
Or you can tell Meteor to update your app to this version by running:
meteor update --release shark-1-29-2014-e
Note that this new templating engine will be included in the Meteor core release by 1.0.
I need to access the entire observable data context from a knockout template, not just it's value.
In the application I'm developing, I often have a lot of meta data that I use to help render views generically. In the past, I've made the view model properties complex - storing both the meta data and data as sub properties (with values in a value property):
ViewModel.AwesomeProperty = {
value: ko.observable('Awesome Value'),
label: 'My Awesome Label',
template: 'awesomeTemplate',
otherMetaData: 'etc'
}
I'm changing this meta data to become properties of the observables (as I believe Ryan Niemeyer described in one of his blog posts or sessions). I find it to be cleaner, more elegant, and generally more maintainable with less overhead (particularly when it comes to serialization). The equivalent to the above example would be as follows:
ViewModel.AwesomeProperty = ko.observable('Awesome Value');
ViewModel.AwesomeProperty.label = 'My Awesome Label';
ViewModel.AwesomeProperty.template = 'awesomeTemplate';
ViewModel.AwesomeProperty.otherMetaData = 'etc';
The side effect of doing this is that passing ViewModel.AwesomeProperty to a template sets the data context to the value of the observable (in this case 'Awesome Value'), making the metadata inaccessible from $data:
<script id="example" type="text/html">
<!-- This won't work anymore -->
<span data-bind="text: $data.label></span>
</script>
<div data-bind="template: {name: 'example', data: AwesomeProperty}"></div>
The workaround I have now is to wrap the data value in an anonymous object like so:
<script id="example" type="text/html">
<!-- Now it works again -->
<span data-bind="text: data.label></span>
</script>
<div data-bind="template: {name: 'example', data: {data:AwesomeProperty}}"></div>
But this is inelegant and not ideal. In a case where there's a lot of auto-generation, this is not only inconvenient, but is actually a major roadblock. I've considered making a custom binding to wrap the template binding, but I'm hoping there's a better solution.
Here's a real world example I've been working for cascading drop downs. This JSFiddle works, but that JSFiddle doesn't.
Thanks in advance.
The thing is that knockout will always use the value after unwrapping it. If it happens to be an observable, you'll lose those sub-properties. You'll have to rewrap your observable into another object so you don't lose it as you have already found.
A nice way you can wrap this up would be to create a function for subscribables (or any of the more derived types) which will do this rewrapping. You can either tack on all individual metadata onto this rewrapped object or pack them into their own separate object. Your code can be elegant again.
var buildSelection = function (choices, Parent) {
return _(ko.observable()).extend({
// add the metadata to a 'meta' object
meta: {
choices: choices,
availableChoices: ko.computed(function () {
if (!Parent) return choices;
if (!Parent()) return [];
return _(choices).where({ ParentID: Parent().ID });
})
}
});
}
ko.subscribable.fn.templateData = function (metaName) {
return {
// access the value through 'value'
value: this,
// access the metadata through 'meta'
meta: this[metaName || 'meta'] // meta property may be overridden
};
}
Then in your bindings, call this function to create the rewrapped object. Just remember to adjust your bindings in your template.
<script id="Selection" type="text/html">
<select data-bind="
options: meta.availableChoices,
optionsText: 'Value',
value: value,
optionsCaption: 'Select One',
enable: meta.availableChoices().length
"></select>
</script>
<!-- ko template: { 'name': 'Selection', 'data': Level1.templateData() } --><!-- /ko -->
<!-- ko template: { 'name': 'Selection', 'data': Level2.templateData() } --><!-- /ko -->
<!-- ko template: { 'name': 'Selection', 'data': Level3.templateData() } --><!-- /ko -->
Updated fiddle