I'm playing around with ember.js and am stuck somehow finding out how to build up the structure the right way. I could follow all examples, but have some problems putting them all together.
I'm using require.js and handlebars.
My directory structure looks like this:
- app
- - controllers
- - css
- - helpers
- - lib
- - models
- - routes
- - templates
- - - partials
- - views
My application.js looks like this:
require.config({
paths:{
jquery:'lib/jquery-1.7.2',
handlebars:'lib/handlebars',
ember:'lib/ember',
ember_data:'lib/ember-data',
text:'lib/requireJS/text',
md5:'lib/md5',
spin:'lib/spin'
},
shim:{
'ember':{
deps:[ 'jquery', 'handlebars'],
exports:'Ember'
},
'ember_data':{
deps:[ 'ember'],
exports:'DS'
}
},
waitSeconds:15
});
define('application'
,[
// Routes
'routes/app_router'
// Controller
,'controllers/application_controller'
// Views
,'views/application_view'
,'views/category/category_list_view'
// Libraries
,'jquery'
,'handlebars'
,'ember'
,'ember_data'
,'spin'
]
, function (
// Router
Router
// Controller
,ApplicationController
// Views
,ApplicationView
,CategoryListView
// Models
,Category
,Product
)
{
return Ember.Application.create({
VERSION: '1.0.0'
,rootElement:'#main'
// Load Router
,Router:Router
// Load Controllers
,ApplicationController:ApplicationController
// Load associated Views
,ApplicationView:ApplicationView
,CategoryListView:CategoryListView
// Load Models
,Category:Category
,Product:Product
//Persistence Layer,using default RESTAdapter in ember-data.js.
,store:DS.Store.create({
revision:10
,adapter:DS.RESTAdapter.create({
bulkCommit:false
,serializer:DS.Serializer.create({
primaryKey:function (type) {
return type.pk;
}
})
,mappings:{
//categories:Category
}
,namespace:'api'
,url: "https://example.org"
})
})
,ready:function () {
}
});
}
);
Then my application controller
define(
'controllers/application_controller'
,['ember' ],
function () {
return Ember.Controller.extend({
init: function() {
}
});
}
);
The application view:
define('views/application_view', [
'text!templates/application.html',
'ember'
],
function(Application_markup) {
return Ember.View.extend({
template: Ember.Handlebars.compile( Application_markup ),
elementId: 'container',
didInsertElement: function() {
this.$().hide().show("slow");
}
});
}
);
And, finally, the application.html template
<div id="container">
<div id="header">
FOO BAR
</div>
<div id="navigation">
{{outlet mainNavigation}}
</div>
<div id="content">
</div>
<div id="footer">
</div>
</div>
What I am trying to do now is to include another template into the main application template (category_list). I guess I either have to do this in the HTML template itself, or in the application view - but in case of the latter one I don't know how to configure/parse/bind more than one template.
What is the best practice of building individual, independent, modular templates and to put them all together? Where exactly should this happen?
Or is this even a wrong approach of using ember.js?
Maybe one of you could make some things more clear to me.
Thanks.
EDIT #1
app_router.js
define('routes/app_router',
['ember' ],
function () {
return Em.Router.extend({
enableLogging:true, //useful for development
/* location property: 'hash': Uses URL fragment identifiers (like #/blog/1) for routing.
'history': Uses the browser's history.pushstate API for routing. Only works in modern browsers with pushstate support.
'none': Does not read or set the browser URL, but still allows for routing to happen. Useful for testing.*/
location:'hash',
/* location: 'history',
rootURL:'/app',*/
root:Ember.Route.extend({
index:Ember.Route.extend({
route:'/'
/*,connectOutlets:function (router) {
//Render application View ,sign in.
v = router.get('applicationController').get('view');
if (v) v.remove();
App.router.get('applicationController').set('loggedin', false);
router.get('applicationController').connectOutlet({name:'login', outletName:'loginform'});
router.get('loginController').enterLogin();
}*/
})
/*,contacts:Em.Route.extend({
route:'/contacts',
showContact:function (router, event) {
router.transitionTo('contacts.contact.index', event.context);
},
showNewContact:function (router) {
router.transitionTo('contacts.newContact', {});
},
logout:function (router) {
jQuery.ajax({
url:'/site/logout',
type:'POST',
success:function (response) {
if (!response.authenticated) {
router.get('applicationController').set('loggedin', false).get('view').remove();
router.transitionTo('root.index', {});
}
}
})
},
index:Em.Route.extend({
route:'/',
connectOutlets:function (router) {
if (router.get('applicationController').get('loggedin'))
router.get('applicationController').connectOutlet('contacts', App.store.findAll(App.Contact));
else router.transitionTo('root.index');
}
}),
contact:Em.Route.extend({
route:'/contact',
index:Em.Route.extend({
route:'/:contact_id',
deserialize:function (router, urlParams) {
return App.store.find(App.Contact, urlParams.contact_id);
debugger;
},
showEdit:function (router) {
router.transitionTo('contacts.contact.edit');
},
connectOutlets:function (router, context) {
if (router.get('applicationController').get('loggedin'))
router.get('contactsController').connectOutlet('contact', context);
else router.transitionTo('root.index');
}
}),
edit:Em.Route.extend({
route:'edit',
cancelEdit:function (router) {
router.transitionTo('contacts.contact.index');
},
connectOutlets:function (router) {
if (router.get('applicationController').get('loggedin')) {
var contactsController = router.get('contactsController');
contactsController.connectOutlet('editContact', router.get('contactController').get('content'));
router.get('editContactController').enterEditing();
} else router.transitionTo('root.index');
},
exit:function (router) {
router.get('editContactController').exitEditing();
}
})
}),
newContact:Em.Route.extend({
route:'/contacts/new',
cancelEdit:function (router) {
router.transitionTo('contacts.index');
},
connectOutlets:function (router) {
if (router.get('applicationController').get('loggedin')) {
router.get('contactsController').connectOutlet('editContact', {});
router.get('editContactController').enterEditing();
} else router.transitionTo('root.index');
},
exit:function (router) {
router.get('editContactController').exitEditing();
}
})
})*/
})
});
}
);
EDIT #2
I changed the router now as follow, but it does not do anything.
define('routes/apps_router', ['ember'],
function () {
return Em.Router.extend({
enableLogging:true
,location:'hash'
,map: function (match) {
match("/").to("CategoryList", function (match) {
match("/").to("mainNavigation");
});
}
,root:Ember.Route.extend({
index:Ember.Route.extend({
route:'/'
,renderTemplates: function() {
this.render('mainNavigation', {
into: 'CategoryList'
});
}
// ....
});
}
);
Kind regards,
Christopher
if you use the latest release of ember with v2 router, you can do something like this:
App.Router.map(function (match) {
match("/").to("categoryList", function (match) {
match("/").to("foo");
});
});
In your catergoryList template, put an {{outlet}} (you can optionally name it)
Then, your route for the template you want to insert into catergoryList will be like this:
App.fooRouter = Ember.Router.extend({
renderTemplates:function () {
this.render('foo', {
into:'catergoryList'
});
}
})
A good example of this in practice can be found here: https://github.com/sh4n3d4v15/ember-todos
Related
The best example to illustrate what I am trying to develop is a desktop email application.
On the left there is a vertical menu (on a quasar q-drawer).
Next, also on the left, there is a mailing list (on a quasar q-list within a q-drawer).
When each item is selected, the corresponding content is displayed on the right (on a quasar q-page).
Expected operation:
The list is loaded once and when I successively select the various items in the list, only the content on the right should be used and the content updated according to the id sent as a parameter in the request.
Note that the list component is only rendered once; that is, it is not rendered again each time a item is selected from the list and remains visible while the content is displayed on the right
The problem:
When I select the first item in the mailing list it works correctly and as expected, the mail content is displayed on the q-page.
When I select a second item from the list it doesn't work anymore and the following error is displayed on the console:
Uncaught (in promise) NavigationDuplicated {_name:
"NavigationDuplicated", name: "NavigationDuplicated", message:
"Navigating to current location ("/mailcontent") is not allowed",
stack: "Error at new NavigationDuplicated
(webpack-int…node_modules/vue/dist/vue.runtime.esm.js:1853:26)"}
I would appreciate suggestions on how to resolve this issue.
The following code is intended to illustrate the problem in the main part:
Routes: secondlayout is the child of another layout
const routes = [
{
path: "/index",
component: () => import("layouts/AppLayout.vue"),
children: [
{ path: "/home", component: () => import("pages/Home.vue") },
{
path: "secondlayout",
component: () => import("Layouts/MailsPlace.vue"),
children: [
{ path: "/mailcontent", name: 'mailcontent', component: () => import("pages/MailContent.vue") },
]
}
]
}
];
Second layout where the email application (list and content) is rendered with q-drawer and router-view
<template>
<q-layout view="lhh LpR lff" container class=" myclass shadow-2 window-height" >
<q-drawer
style="full-height"
v-model="drawerLeft"
:width="500"
:breakpoint="700"
elevated
content-class="bg-grey-1"
>
<q-scroll-area
class="fit"
style="margin-top:80px">
<q-list separator padding>
<q-separator />
<list-mails
v-for="(mail, index) in mails"
:mail="mail"
:key="mail.id_mail"
:id="index">
</list-mails>
<q-separator />
</q-list>
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view></router-view>
</q-page-container>
</template>
<script>
export default {
data () {
return {
mails: {},
drawerRight: false,
}
},
/* watch: {
$route(to, from) {
console.log('after', this.$route.path);
}
},
beforeRouteUpdate(to, from, next) {
console.log('before', this.$route.path);
next();
},*/
components: {
'list-mails': require("pages/ListMails.vue").default,
},
created: function() {
this.listMails()
},
methods: {
listMails(){
this.$axios.get("/listmails")
.then(response => {
if (response.data.success) {
this.mails = response.data.mails.data;
} else {
showErrorNotify('msg');
}
})
.catch(error => {
showErrorMessage(error.message);
});
}
}
</script>
Mail list item with mailitemclick method
<template>
<q-item
clickable
v-ripple
exact
#click="mailitemclick(mail.id_mail)"
>
<q-item-section>
<q-item-label side lines="2"> {{ mail.title_mail }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script>
export default {
props: ["mail"],
methods:{
mailitemclick(id){
this.$router.push({
name: 'mailcontent',
params: {id:id}
});
}
}
}
</script>
Mail content
<template>
<q-page class="fit row wrap justify-center tems-start content-start" style="overflow: hidden;">
<div style="padding:5px; margin:0px 0px 20px 0px; min-width: 650px; max-width: 700px;" >
<q-item>
<q-item-label class="titulo"> {{ mail.title_mail }} </q-item-label>
<div v-html="mail.content_mail"></div>
</q-item>
</div>
</q-page>
</template>
<script>
export default {
name: 'mailcontent',
data() {
return {
mail: {},
};
},
created() {
this.$axios.get(`/mailcontent/${this.$route.params.id}`)
.then(response => {
if (response.data.success) {
this.mail = response.data.mail[0])
} else {
showErrorNotify('msg');
}
})
.catch(error => {
showErrorMessage(error.message);
});
}
}
</script>
This happened to me when I had a router-link pointing to the same route. e.g. /products/1.
The user is able to click on the products, but if a product was
already clicked (and the component view was already loaded) and the
user attempts to click it again, the error/warning shows in the
console.
You can solve this by adding catch block.
methods: {
mailitemclick(id) {
this.$router.push({
name: 'mailcontent',
params: {'id': id}
}).catch(err => {});
}
},
But in the mail-content, you need to use watch for calling function and in mounted for first-time calling.
Temp Example -
data() {
return {
mail: {},
test_mails: {
12: {
content_mail: '<div>test 12<div>'
},
122:{
content_mail: '<div>test 122<div>'
}
}
}
},
mounted() {
this.mail = this.test_mails[this.$route.params.id]
},
watch:{
'$route':function () {
this.mail = this.test_mails[this.$route.params.id]
}
}
OR
You can use :to in list-mail to avoild click and catch -
<q-item
clickable
v-ripple
exact
:to="'/mailcontent/'+mail.id_mail"
>
<q-item-section>
<q-item-label side lines="2"> {{ mail.title_mail }}</q-item-label>
</q-item-section>
</q-item>
children: [
{ path: '', component: () => import('pages/Index.vue') },
{
path: "secondlayout",
component: () => import("layouts/mail-place.vue"),
children: [
{ path: "/mailcontent/:id", name: 'mailcontent', component: () => import("pages/mail-content.vue") },
]
}
]
Sorry if this question is too naive,but I am getting confused a lot on rendering views in Ember.
I have a 'Person' route. I am able to do CRUD operations on it.
router.js
this.route('person', function() {
this.route('index', { path: '' });
});
controllers/person/index.js
actions: {
createPerson: function() {
var person = this.get('store').createRecord('person');
this.set('person', person);
this.set('editPersonPane', true);
},
editPerson: function(person) {
this.set('person', person);
this.set('editPersonPane', true);
},
closeEditPerson: function() {
this.get('person').rollback();
this.set('editPersonPane', false);
},
savePerson: function(person) {
var _this = this;
person.save().then(function() {
_this.set('editPersonPane', false);
Ember.get(_this, 'flashMessages').success('person.flash.personUpdateSuccessful');
}, function() {
Ember.get(_this, 'flashMessages').danger('apiFailure');
});
},
deletePerson: function(person) {
var _this = this;
person.destroyRecord().then(function() {
_this.set('editPersonPane', false);
Ember.get(_this, 'flashMessages').success('person.flash.personDeleteSuccessful');
}, function() {
Ember.get(_this, 'flashMessages').danger('apiFailure');
});
}
}
What I want to do now is when I want to create a new person, a form slides in to create it. After filling up the form, I want the list view of persons to be updated immediately, without refreshing the page. Right now, I have been able to add the form and when I add a new person, I get a successful flash message but it's not updated in the view immediately. I have to refresh the page.
It might have to do something with observers but I am still not sure how.
Reloading a saved object will allow you to avoid having to refresh the page:
savePerson: function(person) {
var _this = this;
person.save().then(function(saved) {
saved.reload();
_this.set('editPersonPane', false);
Ember.get(_this, 'flashMessages').success('person.flash.personUpdateSuccessful');
}, function() {
Ember.get(_this, 'flashMessages').danger('apiFailure');
});
}
Also, it's worth noting that if you destructure and use ES6 syntax, you can clean up your code a bit as follows:
//controllers/person/index.js
//at the top of the file
import Ember from 'ember';
const { get, set } = Ember;
//other code
actions: {
//other actions
savePerson(person): {
person.save().then((saved) => {
saved.reload();
set(this, 'editPersonPane', false);
get(this, 'flashMessages').success('person.flash.personUpdateSuccessful');
}, () {
get(this, 'flashMessages').danger('apiFailure');
});
}
}
Which route is displaying your persons list?
Wouldn't something like this work better, so you can display the list and then edit a person within the persons.hbs outlet?
this.route('persons', function() {
this.route('person', { path: 'id' });
});
I have a layout template but want to apply an alternative layout template. I have created altLayout.html but how do I apply it to my route?
Router.configure({
layoutTemplate: 'layout',
notFoundTemplate: 'pageNotFound',
//waitOn: function() { return Meteor.subscribe('items'); }
});
Router.map(function() {
this.route('main', {
path: '/',
template: 'main',
notFoundtemplate: "pageNotFound",
oldBrowserTemplate: "oldBrowser",
onBeforeAction: function () {
// render the unsupported browser page if user isn't using Chrome
if(BrowserDetect.browser == "Chrome"){
layoutTemplate: 'altLayout',
this.render('oldBrowser');
this.stop();
}
},
});
});
The following works for me:
Router.route("/product/:id",
{
name: "product_page",
template: "product_page",
layoutTemplate: "product_page_layout",
data: function()
{
return {id: this.params.id}
}
});
The "product_page_layout" is where your altLayout.html template goes. Basically:
Router.map(function() {
this.route('main', {
path: '/',
template: 'main',
layoutTemplate: "altLayout",
notFoundtemplate: "pageNotFound",
oldBrowserTemplate: "oldBrowser",
onBeforeAction: function () {
// render the unsupported browser page if user isn't using Chrome
if(BrowserDetect.browser == "Chrome"){
layoutTemplate: 'altLayout',
this.render('oldBrowser');
this.stop();
}
},
});
});
I've recently started using Ember.js with Ember-CLI and I'm pretty excited.
But there are some things that are not clear for me.
Here is my router:
this.resource("authenticated", { path: '/' }, function() {
this.resource("contacts", function() {
this.resource("contact", { path: ':id' });
});
this.resource("chats", function() {
this.resource("chat", { path: ':id' });
});
this.resource("settings", function() {
this.resource("setting", { path: ':id' });
});
});
The question is - why after 2nd nesting 'resolver' starts finding objects outside of 'authenticated' resource?
For example
my-app/pods/chats/index/view
But expected
my-app/pods/authenticated/chats/index/view
Why is 'authenticated' missed ?
Your authenticated route is not applied in the url because you assigned it's url to the root: { path: '/'}.
You should either change the path to 'authenticated' or remove it all together:
this.resource("authenticated", function() {
...
});
Now, however, authenticated is only rendered when a user navigates to my-app/pods/authenticated. If you still want to render authenticated as index, you should prefix your nested resources:
this.resource("authenticated", { path: '/' }, function() {
this.resource("contacts", { path: '/authenticated/contacts' }, function() {
...
});
this.resource("chats", , { path: '/authenticated/chats' }, function() {
...
});
...
});
I hope this helped you.
I'm having a problem doing integration testing with ember using Toran Billup's TDD guide.
I'm using Karma as my test runner with Qunit and Phantom JS.
I'm sure half of if has to do with my beginner's knowledge of the Ember runloop. My question is 2 parts:
1) How do I wrap a vist() test into the run loop properly?
2) How can I test for transitions? The index route ('/') should transition into a resource route called 'projects.index'.
module("Projects Integration Test:", {
setup: function() {
Ember.run(App, App.advanceReadiness);
},
teardown: function() {
App.reset();
}
});
test('Index Route Page', function(){
expect(1);
App.reset();
visit("/").then(function(){
ok(exists("*"), "Found HTML");
});
});
Thanks in advance for any pointers in the right direction.
I just pushed up an example application that does a simple transition when you hit the "/" route using ember.js RC5
https://github.com/toranb/ember-testing-example
The simple "hello world" example looks like this
1.) the template you get redirected to during the transition
<table>
{{#each person in controller}}
<tr>
<td class="name">{{person.fullName}}</td>
<td><input type="submit" class="delete" value="delete" {{action deletePerson person}} /></td>
</tr>
{{/each}}
</table>
2.) the ember.js application code
App = Ember.Application.create();
App.Router.map(function() {
this.resource("other", { path: "/" });
this.resource("people", { path: "/people" });
});
App.OtherRoute = Ember.Route.extend({
redirect: function() {
this.transitionTo('people');
}
});
App.PeopleRoute = Ember.Route.extend({
model: function() {
return App.Person.find();
}
});
App.Person = Ember.Object.extend({
firstName: '',
lastName: ''
});
App.Person.reopenClass({
people: [],
find: function() {
var self = this;
$.getJSON('/api/people', function(response) {
response.forEach(function(hash) {
var person = App.Person.create(hash);
Ember.run(self.people, self.people.pushObject, person);
});
}, this);
return this.people;
}
});
3.) the integration test looks like this
module('integration tests', {
setup: function() {
App.reset();
App.Person.people = [];
},
teardown: function() {
$.mockjaxClear();
}
});
test('ajax response with 2 people yields table with 2 rows', function() {
var json = [{firstName: "x", lastName: "y"}, {firstName: "h", lastName: "z"}];
stubEndpointForHttpRequest('/api/people', json);
visit("/").then(function() {
var rows = find("table tr").length;
equal(rows, 2, rows);
});
});
4.) the integration helper I use on most of my ember.js projects
document.write('<div id="foo"><div id="ember-testing"></div></div>');
Ember.testing = true;
App.rootElement = '#ember-testing';
App.setupForTesting();
App.injectTestHelpers();
function exists(selector) {
return !!find(selector).length;
}
function stubEndpointForHttpRequest(url, json) {
$.mockjax({
url: url,
dataType: 'json',
responseText: json
});
}
$.mockjaxSettings.logging = false;
$.mockjaxSettings.responseTime = 0;
I'm unfamiliar with Karma, but the portions of your test that needs to interact with ember should be pushed into the run loop (as you were mentioning)
Ember.run.next(function(){
//do somethin
transition stuff here etc
});
To check the current route you can steal information out of the ember out, here's some information I stole from stack overflow at some point.
var router = App.__container__.lookup("router:main"); //get the main router
var currentHandlerInfos = router.router.currentHandlerInfos; //get all handlers
var activeHandler = currentHandlerInfos[currentHandlerInfos.length - 1]; // get active handler
var activeRoute = activeHandler.handler; // active route
If you start doing controller testing, I wrote up some info on that http://discuss.emberjs.com/t/unit-testing-multiple-controllers-in-emberjs/1865