I'd like for changes in the URL to drive my application, and for changes in the application to change the URL, but not actually change state.
I have a route like this. The country/city example is a bit contrived, hopefully that doesn't confuse things. The relationship in the real application is somewhat hierarchical. Child views don't seem a fit though because there's no need for nested views.
$stateProvider.state( 'viewMap', {
url: '/viewMap/:country/:city',
templateUrl: 'pages/viewMap/viewMap.html',
controller: 'ViewMapController'
};
In ViewMapController, I can construct the page based on $stateParams.country and .city. As these values change, my application reacts and I want the url to stay in sync. I don't want to reload the whole page, however. I just want to update the url and push a history state on to the stack.
I understand I could manually construct a string:
updateUrl = function() {
window.location.hash = '#/viewMap/'+ $stateParams.country +'/'+ $stateParams.city
}
This feels fragile, as the way I build the string is separate from the way the framework parses it. I would prefer for the framework to build me a string based on the current params, but $state.href('.') describes the current route, which doesn't include $stateParams that haven't yet been activated/navigated to.
I've also looked at reloadOnSearch, but I think it only applies to query params.
Is there a better way to model this? It feels like I'm fighting the framework over something simple.
You can pass state params to $state.href function to get the complete URL
$state.href('.', $stateParams)
To generate arbitrary urls you can pass non-current params and/or configuration:
$state.href('.', {country:'usa',city:'sf'}, {absolute:true})
Related
I'm working with a component bitrix:catalog (which is standard one) and faced an issue. I want to add some extra GET parameters to switch view mode. I think there is no need to rewrite whole component to make such switcher, so I added extra keys in result_modifier in a way similar to:
$this->__component->arResultCacheKeys = array_merge($this->__component->arResultCacheKeys, array('key1', "key2"));
Earlier in the same result_modifier I perform adding those extra keys in $arResult['key1'] etc. They seem to be correctly saved, but only for current inquiry such as ?view=list or view=card, that means only one variable value is saved and it does not react on changing of GET parameter. Is there simple and correct way to make that component to cache and to output data based on GET variable? The only idea which came to my mind is to rewrite component by adding extra parameter and checking of GET, but I think there must more simple and correct solution to make in via template. Human Readable Links are turned on. And I want to have auto-cash being turned on as well. If I turn it off it starts working as planned.
One of possible solutions is to rewrite it cache by SetTemplateCachedData but it still seems to me rough and incorrect way for such simple task.
Bitrix masters please help me to find correct solution, google can't help at the moment.
If you use standard bitrix:catalog component, you may be use standart bitrix:catalog.section. In that component.php used standart component cache.
That means you can describe additional parametr in you custom .parameters.php, and set it in bitrix:catalog.section params.
Standart component cache set cacheId based on arParams.
So you include component should look like this:
$APPLICATION->IncludeComponent(
"bitrix:catalog.section",
"",
array(
"IBLOCK_TYPE" => $arParams["IBLOCK_TYPE"],
"IBLOCK_ID" => $arParams["IBLOCK_ID"],
"ELEMENT_SORT_FIELD" => $arParams["ELEMENT_SORT_FIELD"],
"ELEMENT_SORT_ORDER" => $arParams["ELEMENT_SORT_ORDER"],
....
....
"NEW_ADDITIONAL_GET_PARAMS"=> $_GET['view']
),
$component
);
Of course better way somethink like
"NEW_ADDITIONAL_GET_PARAMS"=> (in_array($_GET['view'],array('list','card'))?$_GET['view']:'list')
But may be you need just set right catalog params: SEF_MODE SEF_FOLDER SEF_URL_TEMPLATES
It may sound like a pedantic question. Sorry :)
I have a case like this... here's my router definition:
Router.map(function() {
this.resource('gobernadores', { path: '/gobernadores' }, function() {
this.resource('gobernador', { path: '/:id_estado' }, function() {
this.route('simulacion', { path: '/simulacion' }),
this.route('info', { path: '/info' })
})
});
this.route("login");
this.route("bienvenido");
});
In the "gobernadores" route, I have list of provinces. You can see it's a nested layout. In that same page, we're showing the currently-selected province (that's the gobernador route). Inside the template for that gobernador route, I have a tab, with two elements..., one showing the route "simulacion", and the other one showing the template of route "info" (of that province).
Now, the problem: as user jumps from one province to another province (by clicking the navigation menu on the left side of the screen), I want to keep in memory, the tab that was currently selected, for each province.
So, if the user is currently seeing the result of simulacion of province X, and then he clicks on the link to go to province Y (where he will be presented with "info" of province Y), and then he goes back to province X, I want the app to take the user back to the screen he was seeing (the simulacion of province X).
You can't have that information stored in the controller (GobernadorController), because I can see that controllers can't keep state, it's stateless.
So..., I have to move that info into the model of the route (GobernadorRouteModel)...
My doubt: is it okay? Why my doubt? Because of this: http://emberjs.com/guides/concepts/core-concepts/
It says:
MODELS
A model is an object that stores persistent state. Templates are
responsible for displaying the model to the user by turning it into
HTML. In many applications, models are loaded via an HTTP JSON API,
although Ember is agnostic to the backend that you choose.
ROUTE
A route is an object that tells the template which model it should
display.
This GobernadorRouteModel is not something I persists in the backend. I have no intention to do that. So, am I violating the general advice for a good EmberJS app?
Or in other words: "persistent" here doesn't have to mean "something you save into DB", right? It's just "something you want to keep around..., eventhough only during the session of the app, in the memory".
Thanks in advance,
Raka
You can't have that information stored in the controller (GobernadorController), because I can see that controllers can't keep state, it's stateless.
This might be where your problem arises. Controllers are not stateless. Controllers in Ember are singletons and keep their state throughout the lifecycle of the app. However, this is going to change in Ember 2.0. To quote from that RFC:
Persistent state should be stored in route objects and passed as initial properties to routable components.
So if you're trying to be forward-compatible, that is the approach I would take. In my opinion, models should really only be used for persistent state (persistent meaning it's persisted between page loads). To keep session state, I would do as the RFC says and keep that state in the routes and inject it into the controllers during the resetController hook.
Or if you don't want to be that fancy and you don't care about forward-compatibility, just have a global Session object that you store state in. That's how I currently do it and it works quite well. (Although we will probably move away from it.)
TL;DR: No, I don't think you're using models for their intended purpose.
So I’m looking to make some routes within my super cool can.js application. Aiming for something like this…
#!claims ClaimsController - lists claims
#!claims/:id ClaimController - views a single claim
#!claims/new ClaimController - creates a new claim
#!claims/:id/pdf - do nothing, the ClaimController will handle it
#!admin AdminController - loads my Administrative panel with menu
#!admin/users - do nothing, the AdminController will handle it
#!admin/settings - do nothing, the AdminController will handle it
So how might we do this?
“claims route”: function() { load('ClaimsController'); },
“claims/:id route”: function() { load('ClaimController'); },
“admin”: function() { load(‘AdminController’); },
Cool beans, we’re off. So what if someone sends a link to someone like...
http://myapp#!claims/1/pdf
Nothing happens! Ok, well let’s add the route.
“claims/:id/pdf route”: function() { load('ClaimController'); },
Great. Now that link works. Here, the router’s job is only to load the controller. The controller will recognize that the pdf action is wanted, and show the correct view.
So pretend I’ve loaded up a claim claims/:id and I edit one or two things. Then I click the Print Preview button to view the PDF and change my route to claims/:id/pdf.
What should happen… the Claim Controller is watching the route and shows the pdf view.
What actually happens… the router sees the change, matches the claims/:id/pdf route we added, and reloads the Claim Controller, displaying a fresh version of the claim pulled from the server/cache, losing my changes.
To try and define the problem, I need the router to identify when the route changes, what controller the route belongs to, and if the controller is already loaded, ignore it. But this is hard!
claims //
claims/:id // different controllers!
claims/:id //
claims/:id/pdf // same controller!
We could just bind on the "controller" change. So defining routes like can.route(':controller') and binding on :controller.
{can.route} controller
// or
can.route.bind('controller', function() {...})
But clicking on a claim (changing from ClaimsController to ClaimController) won't trigger, as the first token claim is the same in both cases.
Is there a convention I can lean on? Should I be specifying every single route in the app and checking if the controller is loaded? Are my preferred route urls just not working?
The following is how I setup routing in complex CanJS applications. You can see an example of this here.
First, do not use can.Control routes. It's an anti-pattern and will be removed in 3.0 for something like the ideas in this issue.
Instead you setup a routing app module that imports and sets up modules by convention similar to this which is used here.
I will explain how to setup a routing app module in a moment. But first, it's important to understand how can.route is different from how you are probably used to thinking of routing. Its difference makes it difficult to understand at first, but once you get it; you'll hopefully see how powerful and perfect it is for client-side routing.
Instead of thinking of urls, think of can.route's data. What is in can.route.attr(). For example, your URLs seem to have data like:
page - the primary area someone is dealing with
subpage - an optional secondary area within the page
id - the id of a type
For example, admin/users might want can.route.attr() to return:
{page: "admin", subpage: "users"}
And, claims/5 might translate into:
{page: "claims", id: "5"}
When I start building an application, I only use urls that look like #!page=admin&subpage=users and ignore the pretty routing until later. I build an application around state first and foremost.
Once I have the mental picture of the can.route.attr() data that encapsulates my application's state, I build a routing app module that listens to changes in can.route and sets up the right controls or components. Yours might look like:
can.route.bind("change", throttle(function(){
if( can.route.attr("page") == "admin" ) {
load("AdminController")
} else if(can.route.attr("page") === "claims" && can.route.attr("id") {
load("ClaimController")
} else if ( ... ) {
...
} else {
// by convention, load a controller for whatever page is
load(can.capitalize(can.route.attr("page")+"Controller")
}
}) );
Finally, after setting all of that up, I make my pretty routes map to my expected can.route.attr() values:
can.route(":page"); // for #!claims, #!admin
can.route("claims/new", {page: "claims", subpage: "new"});
can.route("claims/:id", {page: "claims"});
can.route("admin/:subpage",{page: "admin"});
By doing it this way, you keep your routes independent of rest of the application. Everything simply listens to changes in can.route's attributes. All your routing rules are maintained in one place.
i have create module using module builder , now i am having a field called as book Name
now if i give same book name 2 time t is accepting .
i don't want to use and plug in for checking duplicate value because i want to learn the customization through code .
so i can call ajax and check in data base weather the same book name is exist in db or not but i don't know how controller works in sugar crm . and how to call ajax in sugar crm .
can any one guide me , your help is much appreciated .
If you really want to accomplish this using ajax then I'd recommend an entryPoint as the way to go. This customization will require a couple of simple things. First you'll write a little bit of javascript to perform the actual ajax call. That ajax call will post to the entryPoint you write. The entryPoint will run the query for you and return a response to you in the edit view. So lets get started by writing the entryPoint first.
First, open the file custom/include/MVC/Controller/entry_point_registry.php. If the folder structure and file do not exist yet, go ahead and create them.
Add the following code to the entry_point_registry.php file:
$entry_point_registry['test'] = array('file' => 'custom/test.php', 'auth' => true);
Some quick explanation about that line:
The index value of test can be changed to whatever you like. Perhaps 'unique_book_value' makes more sense in your case. You'll see how this value is used in a minute.
The file value in the array points to where you're gonna put your actual code. You should also give this a more meaningful name. It does NOT need to match the array key mentioned above.
The 'auth' => true part determines whether or not the browser needs to have an active logged in session with SugarCRM or not. In this case (and almost all) I'd suggest keeping this to true.
Now lets look at the code that will go in custom/test.php (or in your case unique_book_name.php):
/* disclaimer: we are not gonna get all crazy with using PDO and parameterized queries at this point,
but be aware that there is potential for sql injection here. The auth => true will help
mitigate that somewhat, but you're never supposed to trust any input, blah blah blah. */
global $db; // load the global sugarcrm database object for your query
$book_name = urldecode($_REQUEST['book_name']); // we are gonna start with $_REQUEST to make this easier to test, but consider changing to $_POST when confirmed working as expected
$book_id = urldecode($_REQUEST['book_id']); // need to make sure this still works as expected when editing an existing record
// the $db->quote is an alias for mysql_real_escape_string() It still does not protect you completely from sql injection, but is better than not using it...
$sql = "SELECT id FROM book_module_table_name WHERE deleted = 0 AND name = '".$db->quote($book_name)."' AND id <> '".$db->quote($book_id)."'";
$res = $db->query($sql);
if ($db->getRowCount($res) > 0) {
echo 'exists';
}
else {
echo 'unique';
}
A note about using direct database queries: There are api methods you can use to accomplish this. (hint: $bean->retrieve_by_string_fields() - check out this article if you wanna go that route: http://developer.sugarcrm.com/2012/03/23/howto-using-the-bean-instead-of-sql-all-the-time/) However, I find the api to be rather slow and ajax should be as fast as possible. If a client asked me to provide this functionality there's a 99% chance I'd use a direct db query. Might use PDO and parameterized query if I'm feeling fancy that day, but it's your call.
Using the above code you should be able to navigate to https://crm.yourdomain.com/index.php?entryPoint=test and run the code we just wrote.
However at this point all you're gonna get is a white screen. If you modify the url to include the entryPoint part and it loads your home page or does NOT go to a white screen there are 3 potential causes:
You put something different for $entry_point_registry['test']. If so change the url to read index.php?entryPoint=whatever_you_put_as_the_array_key
You have sugar in a folder or something on your domain so instead of crm.yourdomain.com it is located somewhere ugly and stupid like yourdomain.com/sugarcrm/ if this is the case just make sure that your are modifying the url such that the actual domain portion is preserved. Okay I'll spell it out for you... https://yourdomain.com/sugarcrm/index.php?entryPoint=test
This is more rare, but for some reason that I cannot figure out apache sometimes needs to be reloaded when adding a new entrypoint. If you have shell access a quick /etc/init.d/apache2 reload should do the trick. If you don't have shell access you may need to open a ticket with your hosting provider (or get a fricking vps where you have some control!!!, c'mon man!)
Still not working? Did you notice the "s" in https? Try http instead and buy a fricking $9 ssl cert, geez man!
Okay moving on. Let's test out the entryPoint a bit. Add a record to the book module. Let's add the book "War of Art" (no, not Art of War, although you should give that a read too).
Now in the url add this: index.php?entryPoint=test&book_name=Art%20of%20War
Oh gawd that url encoding is hideous right! Don't worry about it.
You should hopefully get an ugly white screen with the text "exists". If you do let's make sure it also works the other way. Add a 2 to the book name in the url and hopefully it will now say "unique".
Quick note: if you're using Sugar you're probably also using mysql which is case insensitive when searching on strings. If you really need case sensitivity check out this SO article:
How can I make SQL case sensitive string comparison on MySQL?
Okay so now we have our entryPoint working and we can move on to the fun part of making everything all ajaxical. There are a couple ways to go about this, but rather than going the most basic route I'm gonna show you what I've found to be the most reliable route.
You probably will need to create the following file: custom/modules/CUSTOM_BOOK_MODULE/views/view.edit.php (I hope by now I don't need to point out changing that path to use your module name...
Assuming this file did not exist and we are starting from scratch here is what it will need to look like:
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class CUSTOM_BOOK_MODULEViewEdit extends ViewEdit
{
public function display()
{
// make sure it works in the subpanel too
$this->useForSubpanel = true;
// make the name value available in the tpl file
$this->ss->assign('name_value', $this->bean->name);
// load the parsed contents of the tpl into this var
$name_input_code = $this->ss->fetch('custom/modules/CUSTOM_BOOK_MODULE/tpls/unique_book_checker.tpl.js');
// pass the parsed contents down into the editviewdefs
$this->ss->assign('custom_name_code', $name_input_code);
// definitely need to call the parent method
parent::display();
}
}
Things are looking good. Now we gotta write the code in this file: custom/modules/CUSTOM_BOOK_MODULE/tpls/unique_book_checker.tpl.js
First a couple of assumptions:
We're going to expect that this is Sugar 6.5+ and jquery is already available. If you're on an earlier version you'll need to manually include jquery.
We're going to put the event listener on the name field. If the book name value that you want to check is actually a different field name then simply adjust that in the javascript below.
Here is the code for custom/modules/CUSTOM_BOOK_MODULE/unique_book_checker.tpl.js:
<input type="text" name="name" id="name" maxlength="255" value="{$name_value}" />
<span id="book_unique_result"></span>
{literal}
<script type="text/javascript">
$(document).ready(function() {
$('#name').blur(function(){
$('#book_unique_result').html('<strong> checking name...</strong>');
$.post('index.php?entryPoint=test', {book_name: $('#name').val(), book_id: $('[name="record"]').val()}, function(data){
if (data == 'exists') {
removeFromValidate('EditView', 'name');
addToValidate('EditView', 'name', 'float', true, 'Book Name Must be Unique.');
$('#book_unique_result').html('<strong style="color:red;"> ✗</strong>');
}
else if (data == 'unique') {
removeFromValidate('EditView', 'name');
addToValidate('EditView', 'name', '', true, 'Name Required');
$('#book_unique_result').html('<strong style="color:green;"> ✓</strong>');
}
else {
// uh oh! maybe you have php display errors on?
}
});
});
});
</script>
{/literal}
Another Note: When the code detects that the name already exists we get a little hacky and use Sugar's built in validation stuff to prevent the record from saving. Basically, we are saying that if the name already exists then the name value MUST be a float. I figured this is pretty unlikely and will do the trick. However if you have a book named 3.14 or something like that and you try to create a duplicate this code will NOT prevent the save. It will tell you that a duplicate was found, but it will not prevent the save.
Phew! Okay last two steps and they are easy.
First, open the file: custom/modules/CUSTOM_BOOK_MODULE/metadata/editviewdefs.php.
Next, find the section that provides the metadata for the name field and add this customCode attribute so that it looks like this:
array (
'name' => 'name',
'customCode' => '{$custom_name_code}',
),
Finally, you'll need to do a quick repair and rebuild for the metadata changes to take effect. Go to Admin > Repair > Quick Repair & Rebuild.
Boom! You should be good to go!
In the last three days I've struggled trying to find a way to accomplish what I though was supposed to be a simple thing. Doing this on my own or searching for a solution in the web, didn't help. Maybe because I'm not even sure what to look for, when I do my researches.
I'll try to explain as much as I can here: maybe someone will be able to help me.
I won't say how I'm doing it, because I've tried to do it in many ways and none of them worked for different reasons: I prefer to see a fresh advice from you.
In most of the pages of web application, I have two links (but they could be more) like that:
Option A
Option B
This is partial view, retured by a controller action.
User can select or both (all) values, but they can't never select none of them: meaning that at least one must be always selected.
These links must che accessible in almost all pages and they are not supposed to redirect to a different page, but only to store this information somewhere, to be reused when action needs to filter returned contents: a place always accessible, regarding the current controller, action or user (including non authenticated users) (session? cookie?).
This information is used to filter displayed contents in the whole web application.
So, the problem is not how to create the business logi of that, but how (and where) to store this information:
without messing with the querystring (means: keeps the querystring as empty/clean as possible)
without redirecting to other pages (user must get the current page, just with different contents)
allow this information to persists between all views, until user click again to change the option(s)
My aim is to have this information stored in a model that will contains all options and their selection status (on/off), so the appropriates PartialView will know how to display them.
Also, I could send this model to the "thing" that will handle option changes.
Thanks.
UPDATE
Following Paul's advice, I've took the Session way:
private List<OptionSelectionModel> _userOptionPreferences;
protected List<OptionSelectionModel> UserOptionPreferences
{
get
{
if (Session["UserOptionPreferences"] == null)
{
_userOptionPreferences= Lib.Options.GetOptionSelectionModelList();
}
else
{
_userOptionPreferences= Session["UserOptionPreferences"].ToString().Deserialize<List<OptionSelectionModel>>();
}
if (_userOptionPreferences.Where(g => g.Selected).Count() == 0)
{
foreach (var userOptionPreferencesin _userOptionPreferences)
{
userOptionPreferences.Selected = true;
}
}
UserOptionPreferences= _userOptionPreferences;
return _userOptionPreferences;
}
private set
{
_userOptionPreferences= value;
Session["UserOptionPreferences"] = _userOptionPreferences.SerializeObject();
}
}
Following this, I've overridden (not sure is the right conjugation of "to override" :) OnActionExecuting():
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
GetOptionSelections();
base.OnActionExecuting(filterContext);
}
GetOptionSelections()...
private void GetOptionSelections()
{
if (String.IsNullOrEmpty(Request["optionCode"])) return;
var newOptionCode = Request["optionCode "];
foreach (var userOptionPreferencesin UserOptionPreferences)
{
if (userOptionPreferences.OptionCode == newOptionCode )
userOptionPreferences.Selected = !userOptionPreferences.Selected;
}
}
This code I think can be better, but right now I just want to make it work and it doesn't.
Maybe there are also other issues there (quite sure, actually), but I believe the main issue is that OnActionExecuting is called by each action in a controller that inherit from BaseController, therefore it keeps toggling userOptionPreferences.Selected on/off, but I don't know how to make GetOptionSelections() being called only once in each View: something like the old Page_Load, but for MVC.
Last update AKA solution
Ok, using the session way, I've managed to store this information.
The other issue wasn't really on topic with this question and I've managed to solve it creating a new action that take cares of handling the option's change, then redirects to the caller URL (using the usual returnUrl parameter, but as action parameter).
This way, the option change is done only once per call.
The only thing I don't really like is that I can't simply work with the UserOptionPreferences property, as it doesn't change the session value, but only the value in memory, so I have to set the property with the new object's status each time: not a big deal, but not nice either.
This is a place to use session.
The session will keep your setting between requests while keeping it out of the url querystring. It seems that you have probably tried this already, but try it again and if you have problems ask again. I think it will be the best way for you to solve this problem.