Short version of this question: I'm trying to re-render my ApplicationView when a specific event happens (a language change). The ApplicationView only contains a simple outlet, however on re-rendering this outlet remains empty. So, what is the correct approach to re-render an entire page?
Simplified application code (http://jsfiddle.net/6ZQh7/2/):
Ember.Handlebars.registerHelper('t', function() {
return App.get('language') == 'en' ? 'Hi there!' : 'Hallo!';
});
App = Ember.Application.create({
language: 'en',
ApplicationView: Em.View.extend({
templateName: 'application'
}),
TestView: Em.View.extend({
templateName: 'test'
}),
ApplicationController: Em.Controller.extend(),
Router: Em.Router.extend({
root: Em.Route.extend({
toggleLanguage: function(router) {
App.set('language', App.get('language') == 'en' ? 'nl' : 'en');
// view.parentView resolves to the ApplicationView
router.get('applicationController.view.parentView').rerender();
},
index: Em.Route.extend({
route: '/',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('test')
}
})
})
})
});
Corresponding HTML:
<script type="text/x-handlebars" data-template-name="application">
<h1>{{t whatever}}</h1>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="test">
<h2>My Test Page</h2>
<a href="#" {{action toggleLanguage}}>Toggle language.</a>
</script>
Doing some debugging (well, some; a few hours of it), it seems that when the re-render occurs, the ContainerView that renders the outlet doesn't have a context, which means no currentView (which is bound to this context), which means nothing is rendered in the outlet at all. Scratch that, there is a working context; if I call this.get('templateData.keywords.view.context') in ContainerView#init using the debugger, I get the applicationcontroller. Interestingly enough though, this.get('templateData.keywords.view.context.view') (which should return the view for the outlet) returns undefined, whereas `this.get('templateData.keywords.view.context').get('view') does return the view.
Context: I'm trying to write an internationalized Ember.js app, which contains a simple translation helper outputting strings in the currently set language (used in conjunction with Ember-I18n). Currently changing the language requires a reload of the page, as these language strings are unbound (and I would say rightfully so, as language changes are rare and creating bindings to every string on the page sounds like a bad idea). However, I'd like to just re-render instead of reload.
I am kinda giving the basic approach,
Create language Objects as in...
App.Languages = {};
App.availableLanguages = Em.A([App.Languages.En, App.Languages.Pirate]);
App.Languages.En = Em.Object.extend({
languageName: "English",
appName: "My application Name"
})
App.Languages.Pirate = Em.Object.extend({
languageName: "Pirate",
appName: "!!!Zzzz"
})
App.set('language', App.Languages.En);
Handlebars Helper
Em.Handlebars.registerHelper('loc', function(key){
currentLanguage = Ember.get('language');
value = currentLanguage.get('key');
return Ember.String.htmlSafe(value);
})
Usage:
{{loc appName}}
Changing Language:
A dropdown on the top of page as in
{{Ember.Select contentBinding=App.availableLanguages optionValuePath="content" optionLabelPath="content.languageName" selectionBinding="App.language"}}
Hence, when a language is changed, thanks to ember binding the value gets updated :)
I solved the exact same problem by adding an observer method to the application view, which listens to changes in the language setting, and if it detects a change, rerenders the view using the rerender() method.
I have a language controller:
FLOW.languageControl = Ember.Object.create({
dashboardLanguage:null,
content:[
Ember.Object.create({label: "English", value: "en"}),
Ember.Object.create({label: "Dutch", value: "nl"}),
Ember.Object.create({label: "Spanish", value: "sp"}),
Ember.Object.create({label: "French", value: "fr"})],
changeLanguage:function(){
locale=this.get("dashboardLanguage.value");
console.log('changing language to ',locale);
if (locale == "nl") {Ember.STRINGS=Ember.STRINGS_NL;}
else if (locale == "fr") {Ember.STRINGS=Ember.STRINGS_FR;}
else if (locale == "sp") {Ember.STRINGS=Ember.STRINGS_SP;}
else {Ember.STRINGS=Ember.STRINGS_EN;}
}.observes('dashboardLanguage')
});
with a dropdown in a handlebars file:
<label>Dashboard language: {{view Ember.Select
contentBinding="FLOW.languageControl.content"
optionLabelPath="content.label"
optionValuePath="content.value"
selectionBinding="FLOW.languageControl.dashboardLanguage" }}
</label>
And a (simplified) Navigation view, which renders the top navigation:
FLOW.NavigationView = Em.View.extend({
templateName: 'navigation',
selectedBinding: 'controller.selected',
onLanguageChange:function(){
this.rerender();
}.observes('FLOW.languageControl.dashboardLanguage'),
});
When the navigationView detects a language change (which was caused by the user selecting a language from the dropbox), the navigationView is rerendered.
You could do the same thing on the Application view, it is just that I only need the navigation view to be rerendered.
Related
I'm struggling to understand how to pass data between components in vue.js. I have read through the docs several times and looked at many vue related questions and tutorials, but I'm still not getting it.
To wrap my head around this, I am hoping for help completing a pretty simple example
display a list of users in one component (done)
send the user data to a new component when a link is clicked (done) - see update at bottom.
edit user data and send it back to original component (haven't gotten this far)
Here is a fiddle, which fails on step two: https://jsfiddle.net/retrogradeMT/d1a8hps0/
I understand that I need to use props to pass data to the new component, but I'm not sure how to functionally do it. How do I bind the data to the new component?
HTML:
<div id="page-content">
<router-view></router-view>
</div>
<template id="userBlock" >
<ul>
<li v-for="user in users">{{user.name}} - <a v-link="{ path: '/new' }"> Show new component</a>
</li>
</ul>
</template>
<template id="newtemp" :name ="{{user.name}}">
<form>
<label>Name: </label><input v-model="name">
<input type="submit" value="Submit">
</form>
</template>
js for main component:
Vue.component('app-page', {
template: '#userBlock',
data: function() {
return{
users: []
}
},
ready: function () {
this.fetchUsers();
},
methods: {
fetchUsers: function(){
var users = [
{
id: 1,
name: 'tom'
},
{
id: 2,
name: 'brian'
},
{
id: 3,
name: 'sam'
},
];
this.$set('users', users);
}
}
})
JS for second component:
Vue.component('newtemp', {
template: '#newtemp',
props: 'name',
data: function() {
return {
name: name,
}
},
})
UPDATE
Ok, I've got the second step figured out. Here is a new fiddle showing the progress: https://jsfiddle.net/retrogradeMT/9pffnmjp/
Because I'm using Vue-router, I don't use props to send the data to a new component. Instead, I need set params on the v-link and then use a transition hook to accept it.
V-link changes see named routes in vue-router docs:
<a v-link="{ name: 'new', params: { name: user.name }}"> Show new component</a>
Then on the component, add data to the route options see transition hooks:
Vue.component('newtemp', {
template: '#newtemp',
route: {
data: function(transition) {
transition.next({
// saving the id which is passed in url
name: transition.to.params.name
});
}
},
data: function() {
return {
name:name,
}
},
})
-------------Following is applicable only to Vue 1 --------------
Passing data can be done in multiple ways. The method depends on the type of use.
If you want to pass data from your html while you add a new component. That is done using props.
<my-component prop-name="value"></my-component>
This prop value will be available to your component only if you add the prop name prop-name to your props attribute.
When data is passed from a component to another component because of some dynamic or static event. That is done by using event dispatchers and broadcasters. So for example if you have a component structure like this:
<my-parent>
<my-child-A></my-child-A>
<my-child-B></my-child-B>
</my-parent>
And you want to send data from <my-child-A> to <my-child-B> then in <my-child-A> you will have to dispatch an event:
this.$dispatch('event_name', data);
This event will travel all the way up the parent chain. And from whichever parent you have a branch toward <my-child-B> you broadcast the event along with the data. So in the parent:
events:{
'event_name' : function(data){
this.$broadcast('event_name', data);
},
Now this broadcast will travel down the child chain. And at whichever child you want to grab the event, in our case <my-child-B> we will add another event:
events: {
'event_name' : function(data){
// Your code.
},
},
The third way to pass data is through parameters in v-links. This method is used when components chains are completely destroyed or in cases when the URI changes. And i can see you already understand them.
Decide what type of data communication you want, and choose appropriately.
The best way to send data from a parent component to a child is using props.
Passing data from parent to child via props
Declare props (array or object) in the child
Pass it to the child via <child :name="variableOnParent">
See demo below:
Vue.component('child-comp', {
props: ['message'], // declare the props
template: '<p>At child-comp, using props in the template: {{ message }}</p>',
mounted: function () {
console.log('The props are also available in JS:', this.message);
}
})
new Vue({
el: '#app',
data: {
variableAtParent: 'DATA FROM PARENT!'
}
})
<script src="https://unpkg.com/vue#2.5.13/dist/vue.min.js"></script>
<div id="app">
<p>At Parent: {{ variableAtParent }}<br>And is reactive (edit it) <input v-model="variableAtParent"></p>
<child-comp :message="variableAtParent"></child-comp>
</div>
I think the issue is here:
<template id="newtemp" :name ="{{user.name}}">
When you prefix the prop with : you are indicating to Vue that it is a variable, not a string. So you don't need the {{}} around user.name. Try:
<template id="newtemp" :name ="user.name">
EDIT-----
The above is true, but the bigger issue here is that when you change the URL and go to a new route, the original component disappears. In order to have the second component edit the parent data, the second component would need to be a child component of the first one, or just a part of the same component.
The above-mentioned responses work well but if you want to pass data between 2 sibling components, then the event bus can also be used.
Check out this blog which would help you understand better.
supppose for 2 components : CompA & CompB having same parent and main.js for setting up main vue app. For passing data from CompA to CompB without involving parent component you can do the following.
in main.js file, declare a separate global Vue instance, that will be event bus.
export const bus = new Vue();
In CompA, where the event is generated : you have to emit the event to bus.
methods: {
somethingHappened (){
bus.$emit('changedSomething', 'new data');
}
}
Now the task is to listen the emitted event, so, in CompB, you can listen like.
created (){
bus.$on('changedSomething', (newData) => {
console.log(newData);
})
}
Advantages:
Less & Clean code.
Parent should not involve in passing down data from 1 child comp to another ( as the number of children grows, it will become hard to maintain )
Follows pub-sub approach.
I've found a way to pass parent data to component scope in Vue, i think it's a little a bit of a hack but maybe this will help you.
1) Reference data in Vue Instance as an external object (data : dataObj)
2) Then in the data return function in the child component just return parentScope = dataObj and voila. Now you cann do things like {{ parentScope.prop }} and will work like a charm.
Good Luck!
I access main properties using $root.
Vue.component("example", {
template: `<div>$root.message</div>`
});
...
<example></example>
A global JS variable (object) can be used to pass data between components. Example: Passing data from Ammlogin.vue to Options.vue. In Ammlogin.vue rspData is set to the response from the server. In Options.vue the response from the server is made available via rspData.
index.html:
<script>
var rspData; // global - transfer data between components
</script>
Ammlogin.vue:
....
export default {
data: function() {return vueData},
methods: {
login: function(event){
event.preventDefault(); // otherwise the page is submitted...
vueData.errortxt = "";
axios.post('http://vueamm...../actions.php', { action: this.$data.action, user: this.$data.user, password: this.$data.password})
.then(function (response) {
vueData.user = '';
vueData.password = '';
// activate v-link via JS click...
// JSON.parse is not needed because it is already an object
if (response.data.result === "ok") {
rspData = response.data; // set global rspData
document.getElementById("loginid").click();
} else {
vueData.errortxt = "Felaktig avändare eller lösenord!"
}
})
.catch(function (error) {
// Wu oh! Something went wrong
vueData.errortxt = error.message;
});
},
....
Options.vue:
<template>
<main-layout>
<p>Alternativ</p>
<p>Resultat: {{rspData.result}}</p>
<p>Meddelande: {{rspData.data}}</p>
<v-link href='/'>Logga ut</v-link>
</main-layout>
</template>
<script>
import MainLayout from '../layouts/Main.vue'
import VLink from '../components/VLink.vue'
var optData = { rspData: rspData}; // rspData is global
export default {
data: function() {return optData},
components: {
MainLayout,
VLink
}
}
</script>
What is the correct way to set properties on an Ember route? I have a title message that I would like to be displayed on page load and then I would like to change that title as the user interacts with the page.
import Ember from 'ember';
export default Ember.Route.extend({
title: "Welcome!",
model(thing1) {
return Ember.RSVP.hash({
thing1: this.store.findRecord('thing1', thing1.thing_id),
thing2: this.store.findAll('thing2'),
thing3: this.store.findAll('thing3')
});
},
actions: {
changeTitle() {
this.set("title", "I changed!")
}
}
});
In my template I load another component and pass in the value for title
{{title-tile title=title}}
And in my component title, I print out (theoretically) the title:
{{title}}
I have also tried removing the intermediary step and just printing out the title directly but that doesn't work.
In the console I have no errors, and I am otherwise able to print out the model data from the RSVP hash. There is no (obvious) documentation on this. All documentation focuses on printing out model properties.
What am I missing?
Edit
It appears to me that routes are just meant to handle models and that components are supposed to take care of everything else.
I've actually tried explicitly calling the title to be set on route load and it still doesn't work.
...
init() {
title: "Welcome!"
}
...
You could use a computed property in hash passed to template:
export default Ember.Route.extend({
title: 'Welcome!',
model(thing1) {
return Ember.RSVP.hash({
// ... omitted for brevity
title: Ember.computed(() => this.get('title')) // computed property
});
},
actions: {
changeTitle() {
this.set("title", "I changed!")
this.refresh(); // it is required to refresh model
}
}
});
Working demo.
Full code behind demo.
When using Iron Router with Meteor.js 0.8.3, how can I have a text in a view template that changes depending on which route the user is on?
For example, if a user is at /profile, the text would be User Profile and if he is at / the text will be Home.
header.html
<template name="header">
<h1>{{ routeTitle }}</h1>
</template>
profile.html
<template name="profile">
{{> header}}
</template>
router.js
Router.map( function() {
this.route('index', {
path: '/',
template: 'index'
})
this.route('profile', {
path: '/profile/:_id',
template: 'profile',
data: function() { return Users.findOne(this.params._id); }
})
})
I personally store my own properties in the route options like this :
Router.map(function(){
this.route("index", {
// iron-router standard properties
path: "/",
// custom properties
title: "Home"
//
controller: "IndexController"
});
this.route("profile", {
path: "/profile/:_id",
title: "User profile",
controller: "ProfileController"
});
});
Then I extend the Router with a set of utilities functions to access the current route.
_.extend(Router,{
currentRoute:function(){
return this.current()?this.current().route:"";
}
});
UI.registerHelper("currentRoute",Router.currentRoute.bind(Router));
Using these utilities, you can call Router.currentRoute() in JS which happens to be a reactive data source too, as it acts as a wrapper for Router.current().
Use Router.currentRoute() && Router.currentRoute().options.title to check whether there is a current route and fetch the title you declared in the route definition.
In templates you can use {{currentRoute.options.title}} to fetch the current title, this is helpful when working with iron-router layouts.
If you want to get more specific you can even mimic the Router.path and pathFor helper behavior :
_.extend(Router,{
title:function(routeName){
return this.routes[routeName] && this.routes[routeName].options.title;
}
});
UI.registerHelper("titleFor",Router.title.bind(Router));
Now you can call Router.title("index") in JS and {{titleFor "index"}} in templates.
You can even get as far as having a dedicated helper for the current route title, as you suggested in your question :
UI.registerHelper("currentRouteTitle",function(){
return Router.currentRoute() && Router.currentRoute().options.title;
});
You can achieve this very easily with data param of the path:
Router.map(function() {
this.route('...', {
...
data: function() {
return {
importantText: 'User Profile',
};
},
});
});
Now use it as any other data object in your rendered template, layout template, or any of the templates rendered to named area:
{{importantText}}
I've been trying to figure this out for most of today and it's driving me insane, because I think i'm almost there, but just can't figure the last part out...
I have a route, called Map, which renders a sidebar, and within it has a named outlet for sidebar content:
map.hbs:
<div id="map-container">
{{render sidebar}}
<div id="map-canvas">
</div>
</div>
...
sidebar.hbs:
<div id="content-menu">
{{outlet sidebar-content}}
</div>
Each menu item in my sidebar has a custom action called loadModule, which performs a render of a named view into the sidebar-content outlet (using {{action 'loadModule' 'sidebar.module'}}):
var MapRoute = App.AuthenticatedRoute.extend({
actions: {
loadModule: function(module) {
//load a valid view template into the view
this.render(module,
{
into: 'sidebar',
outlet: 'sidebar-content'
});
}
}
});
module.exports = MapRoute;
Any action within the controller for that view works fine, I can trigger them from buttons etc, or by calling them in a didInsertElement in the SidebarModuleViews.
My issue is that I can't define a model for these views, so if I try and get data from my API in any of their Controllers, it won't render that data out to the templates.
I tried to use link-to, but I couldn't make the template append to the current viewport, rather than refreshing the entire page, which defeats the point of having a sidebar (I don't want the route to change)
var SidebarUserController = App.ApplicationController.extend({
actions: {
doSomething: function() {
alert('SOMETHING');
},
fetchUserProfile: function() {
//do something
var mod = this.store.find('profile', App.Session.get('uid'));
}
}
});
I can trigger either of those actions from the rendered template once it's rendered, however, although my store updates with the record, the handlebars helpers in the sidebar/user.hbs do not populate with the model data.
Here is my model:
var Profile = DS.Model.extend({
uid: DS.attr('string'),
firstName: DS.attr('string'),
lastName: DS.attr('string'),
gender: DS.attr('string'),
DOB: DS.attr('date'),
email: DS.attr('string')
});
module.exports = Profile;
and here is my sidebar/user.hbs:
<div class="container">
<button {{action 'doSomething'}}>Do A Thing</button>
<h1>{{firstName}} {{lastName}}</h1>
<h4>{{id}}</h4>
{{#if isAuthenticated}}
<a href="#" {{action 'logout'}}>Logout</a>
{{/if}}
</div>
In that template, the firstName, lastName and id fields do not populate, even though i'm pulling the data from the API and successfully storing it.
Additionally, if it helps, my router.map for sidebar/user looks like this:
this.resource('sidebar', function() {
this.route('user');
});
I believe that the fundamental issue here is that I can't work out how to set the model for the controller without triggering the route. Am I going about this wrong?
Ok so i've worked this out for my particular instance. It may not be the best way of doing it, but it's what I need:
In my MapRoute, I setup the model and controller for my additional sidebar menus in the setupController function. Doing this allows me to load critical data (such as user profile etc), on page load, and I can still retain the render function for each sidebar module in the Route, which will allow the intial data to load, and still allow me to update the model data for each sidebar module controller on subsequent functions:
map_route.js:
actions: {
loadModule: function(module) {
this.render(module, {into: 'sidebar', outlet: 'sidebar-content'});
}
},
setupController: function(controller, profile) {
var model = this.store.find('profile', App.Session.get('uid'));
var controller = this.controllerFor('sidebar.user');
controller.set('content', model);
},
...
I am writing my first Backbone.js application and I am having some trouble figuring out the best way to program it. I have 2 main views:
Shows an index of all my models.
Shows a specific model for editing.
But #2 has many different 'modules' like I can edit the 'news' section, or 'about' section etc...
All these modules are in a navigation bar.
That navigation bar is hidden when I am displaying view # 1 (index of all models). It is visible in view # 2(a specific model) in order to navigate between different modules.
I have routes setup like this:
routes: {
'', 'index',
'communities': 'index',
'communities/:id': 'main',
'communities/:id/news', 'news',
'communities/:id/about', 'about'
},
So my question is, when 'news' or 'about' action is called, do I add a navigation bar in each method? Isn't that redundant? I am going to have like 8-10 different modules, add navigation bar each time seems very repetitive. Is there a better way?
The only time I want the navigation bar to be hidden is when showing index.
I came across this same problem when I created my first somewhat complex Backbone app. Along with your concern of redundant code, I was concerned about events bound to my navbar that may not get unbound as the navigation bar changed. To solve the problem, I wound up creating a view hierarchy, with one manager view managing the navigation bar a whole, and separate views for each type of navigation menu I wanted to display, which would be passed to the manager view to render to the page.
Here's an example of my implementation.
Before we start, here is a close function I added to Backbone's View prototype which unbinds events and removes the view
Backbone.View.prototype.close = function() {
if(this.beforeClose) { this.beforeClose(); }
this.remove();
this.unbind();
}
First, here is my Manager View. Its render function closes whatever menu is currently displayed and replaces it with the one passed to it as view. While slightly redundant, I created an explicit empty function to make my router code easier to understand.
var App.Views.SubNavBar = Backbone.View.extend({
currentView: null,
el: '#subnav-wrap',
render: function(view) {
if(this.currentView) { this.currentView.close(); }
this.currentView = view;
this.$el.html(view.el);
},
empty: function() {
if(this.currentView) { this.currentView.close(); }
this.currentView = null;
}
});
Second, here is a base view that all of my specific navigation menu views extend. Since they will all have the same tagName, className, id, and initialize and render functions, this keeps repetition to a minimum
var App.Views.SubNavBase = Backbone.View.extend({
tagName: 'ul',
className: 'nav nav-pills',
id: 'subnav',
template: _.template($('#tmpl-subnav').html(),
initialize: function() {
if(this.setLinks) { this.setLinks(); }
this.render();
},
render: function() {
this.$el.html(this.template({links:this.links}));
return this;
}
});
Here is an example of a view for a specific navigation menu. You can see that all I need to do is define the links I want to appear in the menu. When I instantiate this view, the functions of SubNavBase will handle populating the view with the required HTML. Note that I also have some events attached to this view.
var App.Views.Projects.DisplayNav = App.Views.SubNavBase.extend({
setLinks: function() {
this.links = {
'Edit Project': {
icon: 'edit',
class: 'menu-edit',
href: '#projects/'+this.model.get('id')+'/edit'
},
'Add Group': {
icon: 'plus',
class: 'menu-add-group',
href: '#projects/'+this.model.get('id')+'/groups/new'
},
'Delete Project': {
icon: 'trash',
class: 'menu-delete',
href: '#'
}
}
},
events: {
'click a.menu-delete' : 'delete'
},
delete: function(e) {
e.preventDefault();
// here goes my code to delete a project model
}
});
Now, here is the underscore.js template I use to turn the links object above into a list of <li> elements. Note that I use <# instead of <% for my templates since this is a rails app and rails already uses <%
<script type="text/template" id="tmpl-subnav">
<# _.each(links,function(link, title) { #>
<li>
<a href="<#= link.href #>" class="<#= link.class #>">
<i class="icon-<#= link.icon #>"></i>
<#= link.title #>
</a>
</li>
<# }); #>
</script>
Finally, to put it all together, here is an example Router function that creates and renders the nav menu. The steps that occur are as follows:
App.Views.Projects.DisplayNav gets passed a model and populates its this.el with the corresponding HTML, as determined by the underscore.js template
App.SubNavBar has its render function called with the new menu view
App.SubNavBar checks to see if there is currently another menu in the navigation bar; if so, it calls its view's close() function
App.SubNavBar finally appends the passed view's HTML to itself, maintaining a reference to the view for later use
I've included only the relevant parts of the router code
var App.Routers.Projects = Backbone.Router.extend({
routes: {
'projects/:id' : 'display'
},
display: function(id) {
var p = projects.get(id);
var subnav = new App.Views.Projects.DisplayNav({model:p})
App.SubNavManager.render(subnav); // App.SubNavManager is an instance of App.Views.SubNavBar
}
});
The benefit to all of this is that I can now attach events to my menu-specific views, and the manager view will take care of unbinding them if the user navigates to different content and the menu changes.
Of course, there are many other patterns you can use to handle navigation menus, but hopefully this will help you on the path.
Try this:
routes: {
'', 'index',
'communities': 'index',
'communities/:id': 'main',
'communities/:id/:section': 'openSection'
},
openSection : function(id, section){
if( section ){
this.addNavigationBar();
}
switch( section ){
case 'news' :
this.news();
break;
case 'about' :
this.about();
break;
default:
this.main();
}
}
If your url contents a section you will add the navigation bar and then call you normal method as you have.