I have two directives, each displaying a list of documents, but in a slightly different way : One displays user-favorited documents, and the other one displays user-pinned documents. These two properties depend on two object members specified for each document, i.e. :
document = {
pinned: true,
favorite: false
};
Each directive displays a frame with its own title, according to the type of documents we want to display. For refactoring purposes, I use the same template for both, and specify varying template strings and objects in two controllers, each one dedicated to a directive. (i.e. the service member to call to get the documents we want is specified in a string, since the handling of these is exactly the same)
…Until I realized the two controllers were almost identical, the only thing that changed being… template strings.
So what I came up with is using the exact same controller and template (DocumentsPanel), but still with two directives, the only difference in them being link() :
function documentsPanelFavorites(templateService, resources) {
return {
restrict: 'A',
templateUrl: templateService.getUrl('Documents/Panel/documentsPanel.html'),
controller: 'DocumentsPanel',
scope: true,
link: link
};
function link(scope) {
//Used to show a favorite/pinned checkmark for each document entry
scope.documentOptions = {
showFavoriteCheckmark: true,
showPinnedCherkmark: false
};
scope.panelName = resources.text.MODULE_TITLE_FAVORITE_DOCUMENTS;
scope.className = 'favorites';
scope.noDocumentText = 'No favorite for this user';
// Used by the controller to know which method of the
// document dataService to call to get the listed documents
scope.documentList = 'favoriteDocuments'
// etc.
}
};
The documentsPanel.html template then uses these strings defined in the controller's scope via link().
Note: Another directive used to represent a document in a list is included in documentsPanel.html, that's why I set both showPinned and showFavorite options in each directive : It's the same directive that displays each document, and it is used elsewhere with all settings to true.
Would that be considered good practice? If not, what would be a better solution?
Thanks in advance!
I would expect documents="document | filter:{pinned:true}" and documents="document | filter:{favorite:true}"... Considering title, no document text, etc. I would first create config object and pass it to directive: config.title = '...', config.nodoctext = ... But if number of this strings params is too big, just create 2 templates.
Related
I'm working on a dashboard with multiple tiles. Every tile is an XML view. At first, every tile view was supposed to be created once, but due to some new requirements, the user should be able to add the same tile multiple times with different configurations.
To achieve this, I simply tried to create a view like this:
sap.ui.xmlview({
viewName: "com.sap.tiles." + selectedTile + "." + selectedTile
});
The tile shows up correctly, but it seems like it's using the exact same controller as the already existing tile of the same type. Every variable is already set in the controller. Is it possible to instantiate a new controller?
I've read that using the same view multiple times in one window (with different controller instances) is not possible and components or fragments should be used instead. Is that true, or am I doing something wrong?
Final edit:
I've solved my problem. It was a problem very specific to my code, which lead to a wrong function call. Boghyon Hoffmanns answer helped me find the solution.
The tile shows up correctly, but it seems like it's using the exact same controller as the already existing tile of the same type. Every variable is already set in the controller.
You probably declared the variables in the closure outside of the module definition.
sap.ui.define([
"sap/ui/core/mvc/Controller",
// ...
], function(Controller /*, ...*/){
"use strict";
var something; // Visible to all Controller instances of the same name
return Controller.extend("com.sap.tiles...", {
onInit: function() {
something = 1; // Applies to all Controller instances of the same name
this.something = 0; // Applies to this instance only
},
});
});
Creating the same view multiple times does not fetch the corresponding Controller multiple times (since the module has been already defined), but it does instantiate the module (Controller) every time when the view is created.
I am looking for advice on how to implement a hierarchical structure in Angular, where a directive (<partition>) can call a method on a child directive's controller (<property-value>).
I have put together a detailed example here:
https://jsfiddle.net/95kjjxkh/1/
As you can see, my code contains an outer directive, <partition>, which displays one or more <property-value> directives within.
The <property-value> directive offers an editing method, editItem(), which allows the user to change the value of a single entry. (To keep my example short, I simply assign a random number here, but in my production app, a modal will appear, to query the user for a new value.)
This works fine. However, in the outer directive, <partition>, I would like to add the ability to create a new, blank <property-value> directive and then immediately call its editing method so that the user can enter an initial value. If no initial value is entered, the new item would be discarded.
I have seen examples of inner directives calling methods on enclosing directives, but not the other way around.
Is there a way to do this? Alternatively, is there a better way for me to build this kind of view?
You can always use $broadcast to talk both ways. To your parent as well as to your childrens.
In your Child controller you can do the following
app.directive('propertyValue', function() {
return {
require : '^partition'
restrict: 'E',
scope: {
item: '='
},
with this you will get the parent controller in child directive's link function like this
link:function(scope,element,attrs,partitionCtrl){
partitionCtrl.getChildCtrl(element)
}
in partition controller create getChildCtrl function and with that call "propertyvalue" controller function
controller: function ($scope, ItemFactory) {
// your code
var propValueCtrl =undefined;
this.getChildCtrl =function(elem)
{
propValueCtrl = elem.controller();
}
this.callChildFunction = function()
{
propValueCtrl.Edit();// whatever is the name of function
}
call this function when needed in property link function.
Hope this helps.
Now, I know this is an off-the-wall question and that there is probably not going to be any API level access for doing this. I also understand that doing this is completely unnecessary. However, the implementation that I am aiming for requires for me to be able to make {{ variable }} look inside of the $scope.object instead of the $scope itself.
For example:
Controller($scope) {
$scope.active = ...;
}
In this case you can get the active object through {{active}} and any child elements through {{active.broken}} however for the sake of humoring me, lets assume that all of the variables I'm ever going to have to obtain is going to be part of that active object. So I'll be typing things like.. (Data not related)
{{active.title}}
{{active.author}}
{{active.content}}
You could just say "Well why not just move the title/author/content into the $scope and keep it outside of the active object, as that would achieve the desired result of this:
{{title}}
{{author}}
{{content}}
Well, that's where the problem comes in. This is a controller that is not exposed to my end-user, however the end-user does have a completely mutable object (in this example: active) that they can modify. This object has many [optional] listeners and callbacks that are invoked by the application controller when necessary.
The user's that have used my application during testing have commented on what a drag it is to have to type in active. before everything in order to get the data they wanted. Considering data from the $scope is never rendered to the screen, and only data from active is, I was wondering if perhaps there was a way to change where AngularJS looks when parsing/binding data.
If the goal is to evaluate expressions in a different context than $scope, that can be done with the $parse service.
app.controller("myVm", function($scope, $parse) {
var vm = $scope;
vm.active = { a: 5,
b: 3
};
var.myFn = $parse("a+b");
vm.total = myFn(vm.active);
console.log(vm.total);
});
The above example shows how to evaluate the expression a+b using the properties of the $scope.active object.
The DEMO on JSFiddle
I have approximately four main pairs of related directives, simultaneously displayed in my AngularJS UI. Generally speaking, each pair includes a parent list directive on top with an associated detail editor directive beneath it, as shown in this diagram:
Each right-hand list has a many-to-one relationship with the active left-hand selection, and related data must always be displayed. How should I drive related list (the left-to-right association) refreshes?
Currently, each master-detail directive-pair, or "stack", share a service. That service holds the itemState.active (active detail record) and itemState.headers (query master results list). Activity in either the master or detail panel call the service, which directly affect the service state. Then, the master/detail association is operated via simple declarative Angular watches on this common positionService.state; almost no controller code is required. I expect that using the service as the single point of truth here will make it easy for me to integrate near-realtime display in the future, for example via SignalR. This master-detail implementation is only provided here for background, although I welcome improvements:
Master Directive, e.g. position-list.js
templateUrl: "position/position-list.html",
controller: ['$scope', 'positionSvc', function ($scope, positionService) {
$scope.positions = positionService.itemState();
$scope.select = function (position) {
positionService.read(position.id)
};
}
Master Template, e.g. position-list.html
<li ng-repeat="i in itemState.headers" ng-click="select(i)">{{i.title}}</li>
Service, e.g. position-svc.js
this.itemState = {
headers: [],
active: { id: 0 },
pending: httpSvc.pending
};
this.create = function (detail) {
httpSvc.remote("post", detail).then(function (header) {
itemState.headers.unshift(header);
read(detail.id);
});
}
this.read = function (id) {
httpSvc.remote("get", id).then(function (detail) {
itemState.active = detail;
});
}
A similar directive and template exists for the detail. It shares the same service. You get the idea.
Now I'm looking for a maintainable way to handle this left->right eventing following good design practices and well-known AngularJS patterns. I'd like to retain composability of my directives.
To demonstrate I'm not looking for you to do my work for me (and to organize my own thoughts), here are some approaches I came up with, although none have seemed right so far. I'm continuing to document these, check back if you need this section cleaned-up. They are grouped by message path:
leftController -> mainApp -> rightController
part a: leftController -> mainApp
mainApp.$scope.onLeftItemSelected (e.g. via { scope: '&' })
leftController.$scope.$emit within leftController.watch
leftController.$parent.onLeftItemSelected (e.g. AngularJS access parent scope from child controller, but incorrectly creates bottom-up dependency)
part b: mainApp -> rightController
mainApp.$scope.$broadcast
mainApp.rightService.load(leftItemId)
leftController -> mainApp model -> rightController.$watch
share parent scope variable between leftController and rightController
transmit isolated scope from leftController to mainApp using { scope: '=' }; this is Jesús Quintana's solution, below, and here: http://plnkr.co/edit/tyZPziT9VoWDFqHdF8A5?p=preview
leftController -> $route -> rightController
implementing this could be ideal, allowing me to provide deep-linking/bookmarking into my single-page-application, but requires enabling the page to load the parent hierarchy when a child is selected, rather than just the converse (loading the child records of a selected parent item). this requires a greater initial investment than I can currently justify. http://embed.plnkr.co/DBSbiV/preview
leftService -> rightService
Inject the nominationService also into the sibling positionService. Call nominationService.load(positionid) when setting the positionService.activeRecord. Pro: already working. Con: It's wrong. This sibling dependency makes the parent list unreusable.
add a custom Pub/Sub service (https://stackoverflow.com/questions/16235689#16235822, http://jsfiddle.net/ThomasBurleson/sv7D5/)
leftService -> $rootScope.broadcast -> rightService
not ideal, assuming our event string will be unique in the root namespace (but performance issues have been addressed, as discussed here https://stackoverflow.com/questions/11252780#19498009 )
leftService -> mainApp -> rightService
Inject the positionService and nominationService into the mainApp. Add a simple event list to positionService to transmit notification upwards. PRO: this retains the leftService's role as single point of truth for the active record throughout the application (as noted by Martin Cortez, in comments below)
leftService -> $rootScope.$route -> rightController -> rightService
see notes for approach 3
References
pass data between controllers
How to communicate between controllers while not using SharedService between them?
Based on angular pattern design the best way is both directive bind the scope by '=' (Two way binding), and inside the directives make a $scope.#watch to detect changes of the element binding and execute the action function given the variable.
Is the more clean way to communicate inside both directives, angular detect the change and spread out to bot directives.
Sorry , if the answer is useless but the question is not well explain .
so I need to be able to support an (finitely) unlimited number of views in ember.
Basically, I'm creating a survey app. The survey can have an undefined number of steps, hence an undefined number of views/controllers. The number of steps is got from the server, along with the content and the handlebars template (there are only a finite number of possible content types for a step), and I need some way of being able to extend the survey for the different surveys. So basically one ember app handles the entire thing, I've managed to abstract my code for rendering the content types out enough that it doesn't matter for the ember step: i now just need to make it support an unlimited number of steps
Is it possible to have ember views and controllers, but instead of referencing them by name, reference them by array indice? Like normally you'd go App.TestView = …, and router.get('applicationController').connectOutlet('test'), but could I use App.AllView[0] = Ember.View.extend(); and router.get('applicationController').connectOutlet('test')[0] or something? Then I can have a for loop at the start, which grabs the survey data which contains the content types and number of steps, plug it in a view array, and we're away
There might be other ways to achieve this, so any solutions would be great. Creating a new ember app is not an option, the same app needs to service whatever is passed in as a survey object.
You could create an array of view classes that contains the different steps. You can add new views to the array using the handlebars template name.
App.surveyViews = [];
App.surveyControllers = [];
App.surveyViews['firstStep'] = Em.View.extend({ templateName: 'my-handlebars-template' });
App.surveyControllers['firstStep'] = Em.Controller.extend();
Your route can take in the step from the route and use that to look up the appropriate view class and controller to use for the connectOutlet.
step: Em.Route.extend({
route: '/:step',
connectOutlets: function(router, context) {
router.get('applicationController').connectOutlet({
viewClass: App.surveyViews[context.step],
controller: App.surveyControllers[context.step].create(),
context: {}
});
}
})
You can add any extra logic you need for the context, etc., but that would be the basic idea.