Closed kurdin closed 10 years ago
Ok, looks like the best (only?) way to render dynamic partial is do something like this: JS:
var r = new Ractive({
el: document.body,
template: '#template',
data: {
showJSON: function (json) {
return JSON.stringify(json);
},
partials: ['user', 'main', 'settings'],
view: 'main',
user: {
name: 'Sergey', id: 234, email: 'sergey@test.com'
}
}
});
r.on('changeView', function(event) {
event.original.preventDefault();
r.set('view', event.node.attributes.viewname.value);
});
HTML:
{{# view === 'main' }}
{{ >main }}
{{/}}
{{# view === 'user' }}
{{ >user }}
{{/}}
{{# view === 'settings' }}
{{ >settings }}
{{/}}
<!-- {{>main}} -->
<div intro="fade">
<p>this is view 1 from {{ view }}</p>
<p intro="fade">{{ showJSON(user) }}</p>
</div>
<!-- {{/main}} -->
<!-- {{>user}} -->
<div intro="fade">
<p>this is view 2 from {{ view }}</p>
</div>
<!-- {{/user}} -->
<!-- {{>settings}} -->
<div intro="fade">
<p intro='fade'>this is view 3 from {{ view }}</p>
</div>
<!-- {{/settings}} -->
@kurdin Few things going on here.
First is that if you use el: document.body
it's going to replace everything including the <script>
tags with the partials. So if you're going to specify the partials like this, use a container element under body.
Second thing is that the recommend way to do this is blocked by bug found in #941. :(
However, a not so bad way is to do this (see http://jsfiddle.net/9Vja5/40/):
var dynamicPartial = Ractive.extend({
template: function(data){
return '#' + data.partial
}
})
var r = new Ractive({
el: '#container',
template: '#template',
components: {
dynamicPartial: dynamicPartial
},
data: {
showJSON: function (json) {
return JSON.stringify(json);
},
partials: ['p1', 'p2', 'p3'],
partial: 'p2',
user: {
name: 'Sergey', id: 234, email: 'sergey@kurdin.us'
}
}
})
r.observe('partial', function(partial) {
r.merge( 'selected', [ partial ] )
});
I up-leveled it so that you're setting template of component (though didn't change the data property from partial), since it seem to make sense with your example, but you could use the function option with a particular partial too.
I'll post an updated example once #941 is fixed.
This looks strange r.merge( 'selected', [ partial ] )
but it works.
Thank you very much Marty!
@kurdin Here is better example now that we fixed a couple other bugs: http://jsfiddle.net/Q3n9z/1/
Component template:
<script id='dynamicComponent' type='text/ractive'>
<div intro='fade'>
{{>dynamicPartial}}
</div>
</script>
Component js:
var dynamicComponent = Ractive.extend({
template: '#dynamicComponent',
partials: {
dynamicPartial: function(data, parser){
// grab from script tag:
return parser.fromId(data.partial)
}
},
init: function(){
this.observe('partial', function(n,o){
if(n===o) { return; }
this.reset(this.data)
}.bind(this), {init: false})
}
})
Prefect! Thanks again!
@martypdx
Problem with your latest code is init: function()
in dynamicComponent
fires every time I clicked and change the partial. That means, if I have form or any other element with event I want to handle, it will fires multiple times:
Check this out: http://jsfiddle.net/Q3n9z/3/
click "Click me" once, then change the view and click "Click me" again.
Is there any other way to set and handle events from Component?
@kurdin We have an open discussion about lifecycle events #927, but in the meantime I've fixed this issue and components now init
once but complete
on each render/re-render.
Thanks for wading through this! Option functions and re-rendering on reset when partials/template change are new in 0.5
. You've helped ironed out the whole workflow.
@martypdx, thanks for quick fix!
BTW, I changed my code to your first example with dynamicPartial
var dynamicPartial = Ractive.extend({
template: function(data){
return '#' + data.partial
}
})
and it works fine, so why dynamicComponent
, your last example is better? It is a less code with using dynamicPartial
approach.
@kurdin Either works, it just depends on your use case. In this example:
<script id='dynamicComponent' type='text/ractive'>
<div intro='fade'>
{{>dynamicPartial}}
</div>
</script>
There is common markup around the partial, so it makes sense to just change the partial.
If you want the same behavior, but with different templates, you're right this makes sense:
var dynamicTemplate = Ractive.extend({
template: function(data){
return '#' + data.template
}
})
Lastly, if you want to use a dynamicComponent (each option has associated behavior and is a component), you could do this:
var dynamicComponent = Ractive.extend({
template: '<dynamic-component/>',
components: {
'dynamic-component': function(data){
return data.component;
}
}
})
or this would work too:
var dynamicComponent = Ractive.extend({
template: function(data){
return '<' + data.component + '/>'
}
})
Thus far, we've been passing string monikers, but you could be more formal and pass the component itself:
var View1 = Ractive.extend({ name: 'view one', ...})
var View2 = Ractive.extend({ name: 'view two', ...})
var View3 = Ractive.extend({ name: 'view three', ...})
var r = new Ractive({
el: '#container',
template: '#template',
components: {
dynamicComponent: dynamicComponent
},
data: {
showJSON: function (json) {
return JSON.stringify(json);
},
views: [View1, View2, View3],
view: View2,
user: {
name: 'Sergey', id: 234, email: 'sergey@kurdin.us'
}
}
})
Ractive will manage objects in <input>
selections, so {{view}}
will have a reference to the component. You might add a display name as I did above, or you could create an object for each one in your array like { name: 'view one', component: component }
. You would need to adjust input display value and value passed to the dynamic component accordingly.
This code would still be the same:
var dynamicComponent = Ractive.extend({
template: '<dynamic-view/>',
components: {
'dynamic-view': function(data){
return data.view;
}
}
})
But instead of forcing a secondary lookup of the string, you just use the passed in component definition.
This last approach is applicable whether you're using partials, templates or components. For example, you might want to pre-compile the template/partials and pass those in.
@martypdx I am learning ractive and trying to recreate this demo: https://github.com/embercasts/authentication-part-2/ in RactiveJS, so far so good!
Eventually, I want to pre-compile all templates on server and send them to the client as JavaScript. I can use grunt with https://github.com/alisonailea/grunt-ractive-parse right? But the biggest question I have (so far) is it possible to pre-render static html pages on server, lets say I use Dust templates with Express, send them to the client and then add interaction with RactiveJS but only when users event happen or data changed.
For example, lets say I have a long list view with pagination, I want to render first page of the list on server (it will be fast) and serve it to the client as simple HTML. When user clicks on second page link, I want to re-render that second page with RactiveJS. Is it possible? Let say. I will have all data as JSON on that page available for RactiveJS to recreate that list view.
This is defiantly possible with Facebook React JS because it will diff HTML on app start and if there is no changes, it wont touch anything on the page, but if you click on second page link it will re-render list view. I am not sure how RactiveJS handles DOM changes.
Thank you so much for all your help and info. This is a great stuff. I have to learn, learn, learn more! :+1:
I can use grunt with https://github.com/alisonailea/grunt-ractive-parse right
I'm not familiar with that one, but looks doable. There's also @marcello3d ractify and rvc for require.js.
But the biggest question I have (so far) is it possible to pre-render static html pages on server, lets say I use Dust templates with Express,
You can render ractive with express. see https://github.com/MartinKolarik/ractive-render/, https://github.com/zenflow/ractive-express, or google for others
send them to the client and then add interaction with RactiveJS but only when users event happen or data changed.
Yes and no. A requested feature is the ability to "attach" ractive to an existing DOM, but it doesn't exist yet.
You can render an initial page, then use ractive to append:
<ul id='target'>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
new Ractive({
el: '#target',
append: true,
template: '{{#items}}<li>item {{.}}</li>{{/}}',
data: { items: new Array(10) }
})
Any subsequent pages could just add data to items. And the initial page wouldn't have any interactive via ractive.
However, I would strongly suggest you profile to determine whether pre-rendering on the server is necessary. Unless you have a very complex template targeted to lower CPU devices like some smart phones, you're not really gaining much except additional infrastructure and complexity in your architecture.
I know you can render ractive with express but we already using Dust JS in our projects and I think it is more flexible and may be even faster then Mustache templates, at least for server side rendering.
A requested feature is the ability to "attach" ractive to an existing DOM, but it doesn't exist yet.
This would be a very nice feature, I think it will set Ractive JS even more apart from many other libraries. :+1:
You can render an initial page, then use ractive to append
Yes, I understand this, but I think it will create on page load flicker effect on that list, if it is too long or too complicated but I will experiment with this. Thanks!
However, I would strongly suggest you profile to determine whether pre-rendering on the server is necessary. Unless you have a very complex template targeted to lower CPU devices like some smart phones, you're not really gaining much except additional infrastructure and complexity in your architecture.
I agree with you and yes, if you doing just SPA, there is no need to render HTML on server, but if you creating website with a lot interaction functionality, like airbnb.com or findthebest.com you will need a SEO and old plain HTML serve to the client. I don't believe in separate SEO solutions where you serve separate static page only for SEO and SPA for real users. It is too complicated in my opinion. For websites, server rendering is always a better because you can use CACHE and control a million small things. Sure, you can serve to the client just a data and render everything in browser but user experience wont be the same (I think). I do Angular and tried Ember but in my view, they all creates somehow "jerky" and "not-so-smooth" UX if they needs to re-render big portion of the screen. I know most people don't care and wont even notice but I do :) I liked Ractive because it somehow different from anything else I tried. In the future, it would be nice to have feature where you can serve HTML from server and jumpstart interaction with Ractive later without re-rendering the whole thing.
@martypdx Thank you for all your help, but could you please help me with 2 more things :)
I have dynamicPartial
component with login form. On "submit" action, I have function where I do this:
var username = this.get('user.name').trim();
var password = this.get('user.password').trim();
this.set('user.name', username);
this.set('user.password', password);
if (!username || !password) {
this.set('errorMessage', 'Please enter your Username and Password!');
return;
}
set
insted of two, this wont work: this.set({'user.password' : password, 'user.password', password})
this.set('user.name', username)
in scope of dynamicPartial
component, I need to pass user data to parent application, what the best way to do this? Probably, I can do this, by App.set('user', this.get('user'))
but it does not look like correct way of keeping application and component data in sync.@kurdin
is there any, more elegant way to do trim() on properties from UI ? Can I use one set insted of two, this wont work: this.set({'user.password' : password, 'user.password', password})
You can use an observer to do this:
ractive.observe('user.name user.password', function (newValue, oldValue, keypath) {
if (typeof newValue === 'string') {
this.set(keypath, newValue.trim());
}
});
@kurdin
this.set('user.name', username) in scope of dynamicPartial component, I need to pass user data to parent application, what the best way to do this? I can do this, by App.set('user', this.get('user')) but it does not look like right things to do.
Since Ractive doesn't clone the data, if you pass user
object from your app to Ractive, it'll be in sync with Ractive's model, i.e.:
var ractive = new Ractive({
data: {
user: app.user
}
});
ractive.set('user.name', 'Martin');
alert(app.user.name); // Martin
Does that work in your case?
You can use an observer to do this:
Yep, this would work:
this.observe('user.name user.password', function (newValue, oldValue, keypath) {
if (newValue) this.set(keypath, newValue.trim());
});
You can also use computed properties:
new Ractive({
computed: {
'user.displayName' : {
get: '${user.name}',
set: function(name){
this.set('user.name', name.trim())
}
}
}
}
You could wrap that in a reusable form:
function computeTrimmed(property){
return {
get: function(value){
return this.get(property)
},
set: function(value){
this.set(property, value.trim())
}
}
}
new Ractive({
computed: {
'user.displayName' : computeTrimmed('user.name')
'user.displayPassword' : computeTrimmed('user.password')
}
}
this wont work: this.set({'user.password' : password, 'user.password', password})
You can use a hashmap to set. In your example you're setting same property twice
I need to pass user data to parent application, what the best way to do this?
As @MartinKolarik pointed out, you shouldn't need to do anything to keep them in sync
@kurdin -
if you doing just SPA, there is no need to render HTML on server, but if you creating website with a lot interaction functionality, like airbnb.com or findthebest.com you will need a SEO and old plain HTML serve to the client. I don't believe in separate SEO solutions where you serve separate static page only for SEO and SPA for real users. It is too complicated in my opinion. For websites, server rendering is always a better because you can use CACHE and control a million small things.
Ractive strives to be a library so you can use it in the architecture of your choosing, not one dictated by a framework. Even though I don't necessarily agree 100%, doesn't mean you shouldn't be able to do it that way. :)
I do Angular and tried Ember but in my view, they all creates somehow "jerky" and "not-so-smooth" UX if they needs to re-render big portion of the screen. I know most people don't care and wont even notice but I do
Agree, smooth is good. Ractive puts a lot of attention into UI/UX.
In the future, it would be nice to have feature where you can serve HTML from server and jumpstart interaction with Ractive later without re-rendering the whole thing.
@Rich-Harris did some key work in this last release to set us up nicely for this sort of thing. Not sure when, but certainly somewhere ahead.
Does that help in your case?
Well, yes and no! For some reason, all user
properties get update but root property loggedIn
does not get in sync when set from component:
var Contollers = {
loginSubmit: function(event) {
/* deleted some logic */
Contollers.loginPost.call(this);
event.original.preventDefault();
},
loginPost: function() {
var data = this.get('user'),
self = this; // <= self is component scope
$.post('/auth.json', data).then(function(response) {
if (response.success) {
self.set('user.token', response.token); // <= this works, user.token will sync to App.data
self.set('loggedIn', true); // <= this DOES NOT work, loggedIn WONT sync to App.data
console.log('Self loggedIn', self.get('loggedIn')); // -> true
console.log('App loggedIn', App.get('loggedIn') // -> false
}
});
}
};
var model = {
/* more properties here */
user: {
username: '',
password : '',
token: ''
},
loggedIn: false
}
var actions = function() {
this.on('submit', Contollers.loginSubmit);
};
var dynamicPartial = Ractive.extend({
data: model,
template: function(data){
return '#' + data.template
}, init: actions
});
var App = new Ractive({
el: '#app',
template: '#template',
components: {
dynamicPartial: dynamicPartial
},
data: model
});
In $.post('/auth.json', data)
callback function, if I change self.set('loggedIn', true);
to App.set('loggedIn', true);
then console.log('Self loggedIn', self.get('loggedIn'));
will be false
component data will be out of sync.
You can also use computed properties
Thanks, I think this is a good idea.
You can use a hashmap to set. In your example you're setting same property twice
I mistyped example, can you please give simple example how to use hashmap to set multiple properties at the same time?
@kurdin
In $.post('/auth.json', data) callback function, if I change self.set('loggedIn', true); to App.set('loggedIn', true); then console.log('Self loggedIn', self.get('loggedIn')); will be false component data will be out of sync.
That's a little bit different situation. If you modify your data outside of Ractive, then you need to call ractive.update()
, because Ractive caches the data internally. On the other hand, if you use ractive.set()
the data in your app will be updated immediately:
model.loggedIn = true;
var ractive = new Ractive({ data: model });
ractive.set('loggedIn', false);
console.log(ractive.get('loggedIn')); // false
console.log(model.loggedIn) // false
model.loggedIn = true;
var ractive = new Ractive({ data: model });
model.loggedIn = false;
ractive.update(); // notify Ractive
console.log(ractive.get('loggedIn')); // false (true without calling ractive.update())
console.log(model.loggedIn) // false
I mistyped example, can you please give simple example how to use hashmap to set multiple properties at the same time?
ractive.set({
'user.name': name,
'user.password': password
});
Don't pass the model into the component extend:
var dynamicPartial = Ractive.extend({
/* data: model, */ // don't do this!
template: function(data){
return '#' + data.template
}, init: actions
});
See http://jsfiddle.net/y8e5y/ for example of component editing parent data.
@kurdin ractive.set
isn't quite as smart about finding the parent data, you'll need to explicitly pass data into the component in your main template:
<dynamicPartial user="{{user}}" loggedIn="{{loggedIn}}"/>
See http://jsfiddle.net/y8e5y/3/
For passing in multiple props to set, both of these work:
this.set({
user: {
username: 'bob',
password: 'sekrit'
},
loggedIn: true
})
this.set({
'user.username': 'bob',
'user.password': 'sekrit',
loggedIn: true
})
But they're subtly different in that the first replaces the entire user
object whereas the second updates the two properties on the original user object.
@martypdx well spotted!
@kurdin My bad, I didn't read your code carefully. If you have 2 Ractive instances, there is no need to call update()
.
Don't pass the model into the component extend: /* data: model, */ // don't do this!
Wow, this fixed everything! Thanks! @MartinKolarik you were wrong :)
<dynamicPartial user="{{user}}" loggedIn="{{loggedIn}}"/>
I don't wont to do it because dynamicPartial
is dynamic component with different views.
For passing in multiple props to set, both of these work
I bet I tried that :) I will try again, thanks for explanation.
That's a little bit different situation. If you modify your data outside of Ractive, then you need to call ractive.update(), because Ractive caches the data internally. On the other hand, if you use ractive.set() the data in your app will be updated immediately:
@MartinKolarik , yes I understand this but as you said before I can sync 2 component's data with data property linked to same data object, it works but not all the time and update wont help. As @martypdx pointed out, you don't need to set data on child component, if component not isolated it get data from parent.
Thank you guys, for all your help, this is priceless.
If you have 2 Ractive instances, there is no need to call update().
Yep! Snap, looks like I am out of questions :)
I don't wont to do it because dynamicPartial is dynamic component with different views.
Is data changing? If not you can pass data this way
Wow, this fixed everything!
But if it is working, great!
But if it is working, great!
It works, so I don't have to pass anything.
@martypdx Unfortunately I still have a problems with data that sets in component view did not update automatically to parent App. I think this is a bug in ractive.
In example http://jsfiddle.net/9Vja5/46/ user.username did gets update from component to App but other properties do not. This is important get this data in sync from child to parent.
In other hand, if I pass data: model
to dynamicPartial
and move loggedIn
to user
object, then data gets in sync from child to parent App. Very strange behavior.
var model = {
user: {
username: '',
password: '',
token: '',
loggedIn: false
},
}
var dynamicPartial = Ractive.extend({
data: model,
/* ..... removed ..........*/
});
var App = new Ractive({
el: '#container',
template: '#template',
components: {
dynamicPartial: dynamicPartial
},
data: model
})
@kurdin You have a couple of options.
As you noticed you can pass the model to both. But it will only sync if you call update()
or when you call ractive.get
on the data props. In other words, the data will be in sync because you're using the same js objects, but any template bound to the parent will need to be told to update via App.update()
. Maybe that's fine for your workflow because you have clearly defined exit points from your dynamic views.
I assume you're going to transition to another view and change dynamic partial, so the data will change via ractive.reset(data)
? Otherwise not sure what difference is between assigning data to component or passing through as component parameters.
You can also give ractive some clues in the template and that will give you the binding you are looking for:
<form class="form-inline" on-submit="submit">
{{#with user}}
<input placeholder="Username" type="text" value="{{ username }}">
<input placeholder="Password" type="text" value="{{ password }}" >
{{/with}}
{{#loggedIn}}{{/}}
<input class="btn" type="submit" value="Log In">
</form>
Basically by using the refs, you force the component to lookup the props in the parent and that establishes the bidrectional link. (see http://jsfiddle.net/9Vja5/55/)
Lastly, since we fixed the bug, you don't need to use the [selected]
workaround and the App.observe
, just use the component (check out the fiddle above)
Wow, this works like a magic :) is there any documentaion about what #with
does ?
{{#with user}}
<input placeholder="Username" type="text" value={{ user.username }}>
<input placeholder="Password" type="text" value={{ user.password }} >
{{/with}}
{{#loggedIn}}{{/}}
Lastly, since we fixed the bug, you don't need to use the [selected] workaround and the App.observe, just use the component (check out the fiddle above)
Well, in your example (http://jsfiddle.net/9Vja5/55/) dynamic view switch does not work if you click on select p1 p2 p3 until I add:
App.observe('partial', function(partial) {
App.merge( 'selected', [ partial ] )
});
and
{{ #selected }}
<dynamicPartial partial='{{ partial }}'/>
{{/}}
is there any documentaion about what #with does ?
We're a bit behind getting the 0.5
docs up-to-date. Basically, you can either use standard mustache blocks which will infer what type of block it is: {{#user}}
and in this last release we borrowed from handlebars syntax so you can explicitly state what type of block:
{{#with user}}
- Object context
{{#each items}}
- iterate
{{#if condition}}
- conditional
{{#unless condition}}
- inverted (same as {{^condition}}
)
We also added {{else}}
.
Well, in your example (http://jsfiddle.net/9Vja5/55/) dynamic view switch does not work
Ooops, forgot to add the reset into the component:
var dynamicPartial = Ractive.extend({
template: function(data){
return '#' + data.partial
},
init : function() {
this.on('submit', loginSubmit);
this.observe('partial', function(n,o){
if(n===o) { return; }
this.reset(this.data)
}.bind(this), {init: false})
}
})
Which is also nicer because the component logic is self-contained.
For some reason, in my case this wont work well with dynamic content pulled and applied to model after ajax call:
this.observe('partial', function(n,o){
if(n===o) { return; }
this.reset(this.data)
}.bind(this), {init: false})
I will post my full example later.
About {{#with user}}
seems to me like a workaround of bug.
Those 2 must have identical behavior but they not:
<input placeholder="Username" type="text" value={{ user.username }}>
<input placeholder="Password" type="text" value={{ user.password }} >
or
{{#with user}}
<input placeholder="Username" type="text" value={{ username }}>
<input placeholder="Password" type="text" value={{ password }} >
{{/with}}
must be the same, but in the first case only user.username data will sync with parent. I am sure they will perform the same thing inside on single component or main app.
About {{#with user}} seems to me like a workaround of bug.
Added #960 last night. :)
@kurdin closing this issue as original issue has been addressed. Feel free to reopen if not, or file new issues you have.
@martypdx now everything works without {{#with user}} very nice!
Here is one more thing I found:
In templates, I have a habit to add extra space before {{
and }}
like this {{ title }}
this usually helps to mix mustache front end templates with backend dusts js templates. Dust will ignore curly brackets with space between content. Ractive template works inside dustjs server template in most of the cases with adding spaces but this is throw error:
<h2>Articles</h2>
{{ #each pages.articles }}
<article>
<h3>{{ title }}</h3>
<p>Posted by {{author}}</p>
<p>{{ body }}</p>
</article>
{{ /each }} <= error because of extra space before and after /each
{{ >accessError }}
all endings of block elements in ractive templates must have no space, everything else work fine with extra spaces, this would work fine:
{{ #if loggedIn }}
<p>Hey <b>{{ user.username }}</b>! You are already logged in.</p>
<a href="#/logout" on-click="logout">Log out</a>
{{ else }}
{{ >login_form }}
{{ #if errorMessage }}
<div class="alert alert-error" intro="fade">{{ errorMessage }}</div>
{{/if}}
{{/if}}
can we fix this, so I can use {{ /each }}
or {{ /if }}
in templates
@kurdin Please create a separate issue for that. Don't know if there's a reason or not (I'm not as familiar with the parser), but others can chime in.
New issue #983
I am trying to get started with Ractive but could not make simple partials template switcher to work. I know, this has been discussed before but still, could not understand when
setTemplate
called inside Ractive Component and how to use it to switch component's template.So, what is
setTemplate
when it is called and how to use it ? Full, not working example is here: http://jsfiddle.net/9Vja5/27/Thanks for help!