Dynamically creating outlets - javascript

Let's say I have an application with two routes: "/services", and "/services/:service".
When a user visits the :service state, I'd like him to see a list of information pertaining to one particular service. So far, trivial.
When the same user visits /services, however, I'd like to show a list of services, plus the per-service information (for every service). I'd also like to do this without any template duplication.
The obvious answer seemed to be outlets, however it looks like all outlets have to be statically named, and I have a previously-undetermined number of them. They all have unique names, but of course I can't hardcode the names in the template. I'd like to determine the outlet names dynamically, so I can call this.render appropriately in the renderTemplate to fill them in, but I don't know how to do that.
What would you suggest?
UPDATE: It appears that I can handle my particular case using {{template}}, but that doesn't connect up another route, as such. I'd still like to know how to do that; I'll be doing more complex things later, where that would definitely fit better, and there's still duplication of code in the respective controllers.
UPDATE: I eventually made up my own 'dynOutlet' helper, as such:
// Define "dynamic outlets". These are just like normal outlets,
// except they dereference the outlet name.
Handlebars.registerHelper('dynOutlet', function(property, options) {
Ember.assert("Syntax: {{dynOutlet <property>}}", arguments.length === 2);
var context = (options.contexts && options.contexts[0]) || this,
normalized = Ember.Handlebars.normalizePath(
context, property, options.data),
pathRoot = normalized.root,
path = normalized.path,
value = (path === 'this') ? pathRoot : Ember.Handlebars.get(
pathRoot, path, options);
return Ember.Handlebars.helpers.outlet(value, options);
});
I've yet to figure out if it'll work the way I want, though.

I had also the need to define dynamic Outlets. This is the helper i implemented:
Ember.Handlebars.registerHelper('dynamicOutlet', function(property, options) {
var outletSource;
if (property && property.data && property.data.isRenderData) {
options = property;
property = 'main';
}
outletSource = options.data.view;
while (!(outletSource.get('template.isTop'))){
outletSource = outletSource.get('_parentView');
}
options.data.view.set('outletSource', outletSource);
var firstContext = options.contexts.objectAt(0);
options.hash.currentViewBinding = '_view.outletSource._outlets.' + firstContext.get(property);
var controller = App.__container__.lookup("controller:movies");
options.contexts.clear();
options.contexts.addObject(controller);
return Ember.Handlebars.helpers.view.call(controller, Ember.Handlebars.OutletView, options);
});
This is the way i use it in my templates:
<div>
{{title}} <!-- accessing title property of context -->
{{dynamicOutlet outletName}} <!-- accessing the property that should be used as name of the outlet -->
</div>
The helper makes it possible to use properties of models as outlet names.

Related

How can I create data-driven text inputs with Ember?

I am trying to create several inputs that update my Ember Data model immediately, without a submit action. E.g.
Person
First Name [Placeholder: Enter first name ]
Last Name [Placeholder: Enter last name ]
City [Placeholder: Enter city ]
I’ve approached this by creating a wrapper component around an Ember TextField subclass component, and have gotten it to work, kind of, but with two major problems:
Placeholders don't work as I tried to implement them (see below) — seems I could do this a lot more easily using the TextSupport mixin, but I don't know how to use it.
I don't like the way I am sending change actions up to the route, which is basically like this:
Within a top-level component {{person-view action="modifyPersonInternals"}} I have my wrapper components as follows:
{{editable-property property="firstName" action="updateFirstName"}}
{{editable-property property="lastName" action="updateLastName"}}
{{editable-property property="city" action="updateCity"}}
When the user edits the firstName, the TextField component calls updateFirstName on the editable-property wrapper component with a parameter (the new value); the wrapper component in turn calls modifyPersonInternals on the route with two parameters (the property to modify, and the new value). This works but seems clunky/hacky and can't be the right approach, can it?
Also, here's my unsuccessful attempt to implement a placeholder in editable-property.js,
hasEmptyProperty: Ember.computed(function() {
return ((this.get('property') === "") || (this.get('property') === null));
})
and in editable-property.hbs:
{{#if hasEmptyProperty}}
<div class="placeholder" {{action "editProperty"}}>{{placeholder}}</div>
{{else}}
<div {{action "editProperty"}}>{{bufferedProperty}}</div>
{{/if}}
I get an error that hasEmptyProperty is not defined.
If you are passing the model into the component anyway, why not just directly associate the models property with your input?
{{input value=model.firstName}}
This will achieve your goal, but if you are trying not to pass the model into the component then the best way is to send an action as you are doing.
With regard to the hasEmptyProperty I would instead have a property called hasEmptyProperty and a function that sets it, something like...
checkEmptyProperty: function() {
var property = this.get('property');
if(property === "" || property === null ) {
this.set('hasEmptyProperty', true);
} else {
this.set('hasEmptyProperty', false);
}.observes('property'),

Getting a list of routes in Ember.js

On my main page (index.hbs), I need to display a list of links to each route which matches a given criteria, (e.g. has a certain attribute). So what I need is something like this:
// Define a route with some attribute
App.FirstRoute = Ember.Route.extend({
showOnIndex: true
});
// Get a list of visible routes in the IndexController
visibleRoutes: function() {
var routes = /* How to do this */
return routes.filter(function(route) {
route.get('showOnIndex'));
});
}.property()
The problem is how to get routes. I have found that I can get a list of route names by:
var router = this.get('target');
var names = router.router.recognizer.names;
But I can't find out how to translate them into Route objects. Presumably the Router has the information to do so, but AFAICS it is not publically exposed. Any suggestions?
what about using the container and lookups?
Ember.keys(App.Router.router.recognizer.names).map(function(name) {
return App.__container__.lookup('route:' + name);
}).compact();
I know you access the __container__ which is private. But normally you shouldn't access all the routes anyway.
Here is one way to do it although I think it is a little bit horrible and maybe even dangerous long term since it is using features of Javascript rather than the Ember API. We start by getting the routes through your 'App' object. You can get a list of all the classes it contains using Object.keys(App). Then you can go through each of these, check if it is a route with .superclass which will be Ember.Route if it is a Route. You can then check the showOnIndex property on the prototype of each of these.
var allRoutes = [];
Object.keys(App).forEach(function(key, index, allKeys) {
if (App[key].superclass === Ember.Route && App[key].prototype.showOnIndex === true) {
allRoutes.push(App[key]);
}
});
We now have an array of all the class names of the routes which have showOnIndex of true.
However, you still have the problem of aligning the names from the recognizer with the class names of the Route but since the Ember way is that a route like my/url will map to MyUrlIndexRoute and 'my.url.index' etc then you can split the Route by upper case, remove the last part ('Route'), join them together and convert it all to lower case.
var routesByName = [];
allRoutes.forEach(function(name, index, all) {
var names = name.match(/[A-Z][a-z]+/g);
names.pop();
var routeName = names.join('.').toLowerCase();
routesByName.push(routeName);
});
Then you can just loop through the routesByName in the template and create a {{#link-to}} for each of them.
I think a better way to achieve what you want may be to keep a map of all the routes to their showOnIndex value separately inside your app and just update it for each new one you want to add. It may be better safer long term since who knows how the Ember internals will change in the future which could prevent doing some of the things above.
Note that I checked all this in debugger mode in an Ember app in chrome but haven't run all the code fragments directly so apologies for any mistakes.
In ember v4.9 you can do the following in any component:
export default class myComponent extends Component {
#service router;
#computed
get routes() {
// load RouterService
let router_service = this.router;
// extract routes and format.
let routes = router_service._router._routerMicrolib.recognizer.names;
let detailed_routes = Object.keys(routes)
.filter((key) => {
console.log(key === "error" )
return ! (key.match(/error/) || key.match(/loading/)
|| key.match(/index/) || key.match(/application/))
})
.map((route) => router_service.urlFor(route));
console.log(detailed_routes);
return detailed_routes
}
}

Best way to wrap ember-data models with their controllers

I have a custom view with a render function that needs to do some calculations. Since I've put all my display logic and properties that the app does not need to save or get on to the server in an ObjectController I need to manually "wrap" my model with the controller to get some computed properties. It works, but isn't there a better/cleaner way? So current code in the view is:
...
currentPage = pages.filterBy('nr', pageNb).get('firstObject')
currentPageCntl = #cntl.get('controllers.page').set('model',currentPage)
currentPageDimensions = currentPageCntl.get('dimensions')
...
So if I understand you correctly, you have logic and data that you don't want to include in your model, even though they belong together in certain places. I'm actually working on an issue very similar to this right now. I don't know if this is the best way to do things, but the way I've been doing it is to wrap the Ember-Data model is an object that more closely represents the model that you want. For instance, here's what that might look like for you:
App.Page = DS.Model.extend
App.PageWrapper = Ember.Object.extend
page: null
dimensions: () ->
# ...
.property('page')
So for your application, don't treat the page like your model, treat the pageWrapper as your model. So change your currentPage to:
currentPage = App.PageWrapper.create
page: pages.filterBy('nr', pageNb).get('firstObject')
This way, you can add whatever logic/models you want to the wrapper class, but still keep it distinct from your page. I might be able to help you come up with something more Ember-like if you gave me some more info, but this is a perfectly valid solution.

Saving with angular $resource.save causes view to redraw/reflow due to collection watcher

I have a model which I load and store using $resource. The model is an aggregate and has nested collections inside, which are binded to an html view using ng-repeat.
Model:
{
someRootField: "blabla",
sectionCollection: [
{
name: "section1"
....
},
{
name: "section2",
....
}
]
}
html:
<div ng-repeat="section in myModel.sectionCollection">
...
</div>
controller:
MyModelResource = $resource(config.api4resource + 'models/:id', {id:'#_id'});
$scope.myModel = MyModelResource.get({id: xxxx});
The problem: when I use $save on this model, it causes a reload/redraw of some portions of the screen (seems not the root fields, but the collection related ones), if some binded elements within the sections are inputs, focus is lost too. I did some debugging and here is what I think is happening.
When I save the model, the results from the POST command mirror the body of the request, and myModel is being repopulated with it. Simple fields in the root of the model are pretty much the same, so the watch() mechanism doesn't detect a change there, however the the objects in the sectionCollection array are different, as they are compared not by their contents but by an equality of the references and fail, this causes the ui controls associated with the collection to be completely reloaded/redrawn.
There is this code in $watchCollectionWatch() in angular:
} else if (isArrayLike(newValue)) {
if (oldValue !== internalArray) {
// we are transitioning from something which was not an array into array.
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
newLength = newValue.length;
if (oldLength !== newLength) {
// if lengths do not match we need to trigger change notification
changeDetected++;
oldValue.length = oldLength = newLength;
}
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
}
in my case, I've definitely seen the oldValue[i] = newValue[i] comparison fail, the objects were different. One of the reason is oldValue contained variables prefixed with $ that were referring back to the scopes that were previously created for each item.
The question is, how can I prevent a reflow? Or how can I do it differently to avoid it. Keeping myself two copies of the model, one for $resource and another for binding to view and synchronizing between them manually does not seem right.
Thanks!
You can use $http service to avoid model updates that $save cause:
$scope.save2 = -> $http.get 'blah_new.json'
I used get in example but you can use whatever you need from this list of shortcut methods. And here is a simple example plunk.
Also it's simple to save elemen's focus after rerendering:
$scope.save = ->
active = document.activeElement.getAttribute 'id'
$scope.user1.$save ->
document.getElementById(active).focus()

How to bind deeper than one level with rivets.js

rivets.js newbie here. I want to bind to an item that will be changing dynamically (store.ActiveItem). I tried the following approach, but although store.ActiveItem is set, store.ActiveItem.(any property) is always undefined. Is there a standard way to bind deeper than one level?
<div id="editItemDialog" data-modal="store.ActiveItem < .ActiveItem">
<a data-on-click="store:ClearActiveItem" href="#">close - works</a>
<div>
<div>
<label>name:</label><input data-value="store.ActiveItem.Name < .ActiveItem"/>
</div>
<div>
<label>price:</label><input data-value="store.ActiveItem.Price < .ActiveItem"/>
</div>
<div>
<label>description:</label><textarea data-value="store.ActiveItem.Description < .ActiveItem"></textarea>
</div>
</div>
</div>
How the binding works is largely determined by the Rivets adapter you are using, although your model could also do the heavy lifting.
Option 1: Smart Model
If you're using Backbone.js, you could take a look at backbone-deep-model, which supports path syntax for nested attributes (Eg. store.get('ActiveItem.Price')), though it is still under development. If that doesn't quite meet your needs, there are other nested model-type options on the Backbone plugins and extensions wiki.
Option 2: Smart Adapter
If that doesn't work for you, you can extend your Rivets adapter to handle path syntax. I've put together a simple example on how to do this at http://jsfiddle.net/zKHYz/2/ with the following naive adapter:
rivets.configure({
adapter: {
subscribe: function(obj, keypath, callback) { /* Subscribe here */ },
unsubscribe: function(obj, keypath, callback) { /* Unsubscribe here */ },
read: function(obj, keypath) {
var index = keypath.indexOf('.');
if (index > -1) {
var pathA = keypath.slice(0, index);
var pathB = keypath.slice(index + 1);
return obj[pathA][pathB];
} else {
return obj[keypath];
}
},
publish: function(obj, keypath, value) {
var index = keypath.indexOf('.');
if (index > -1) {
var pathA = keypath.slice(0, index);
var pathB = keypath.slice(index + 1);
return obj[pathA][pathB] = value;
} else {
return obj[keypath] = value;
}
}
}
});
Option 3: Dirty Hacks
As of version 0.3.2, Rivets supports iteration binding. By creating a Rivets formatter that returns an array, you can "iterate" over your property. Take a look at http://jsfiddle.net/mhsXG/3/ for a working example of this:
rivets.formatters.toArray = function(value) {
return [value];
};
<div data-each-item="store.ActiveItem | toArray < store.ActiveItem"">
<label>name:</label><input data-value="item.Name < store.ActiveItem"/>
...
</div>
I'm not sure if the computed property syntax is required here; you will have to test this with your model to see what works.
Option 4: Don't bind deeper than one level (Recommended)
The need to bind deeper than one level may be an indication that your design can be improved.
In your example, you have a list of Items in an ItemCollection for a Store. You go about assigning a single Item to the Store's ActiveItem property, setting up events everywhere to try link things together, and then need to be able to bind to the properties of the ActiveItem under the Store, yet have things update whenever the ActiveItem itself changes, etc..
A better way of doing this is by using a view-per-model approach. In your example, you're trying to handle the Store Model, the ItemCollection and the Item Model with a single view. Instead, you could have a parent Store view, a subview for the ItemCollection, and then generate Item views as necessary below that. This way, the views are easier to build and debug, less tightly coupled to your overall Model design, and are more readily reusable throughout your application. In this example, it also simplifies your Model design, as you no longer need the ActiveItem property on the Store to try maintain state; you simply bind the Item View to the selected Item Model, and everything is released with the Item View.
If you're using Backbone.js, take a look at Backbone.View as a starting point; there are many examples online, although I'll be the first to admit that things can get somewhat complex, especially when you have nested views. I have heard good things about Backbone.LayoutManager and how it reduces this complexity, but have not yet had the chance to use it myself.
I've modified your most recent example to use generated Item views at http://jsfiddle.net/EAvXT/8/, and done away with the ActiveItem property accordingly. While I haven't split the Store view from the ItemCollection view, note that I pass their Models into Rivets separately to avoid needing to bind to store.Items.models. Again, it is a fairly naive example, and does not handle the full View lifecycle, such as unbinding Rivets when the View is removed.

Categories

Resources