Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

how do you handle state for a modal dialog ? #123

Closed
graphicsxp opened this issue May 7, 2013 · 25 comments
Closed

how do you handle state for a modal dialog ? #123

graphicsxp opened this issue May 7, 2013 · 25 comments

Comments

@graphicsxp
Copy link

I use ui-router for handling state management in my application.

In one of my states, the view contains a form for filling data about a Contract and inside that form there is a directive .

The directive contains a search button for opening a modal dialog. Inside that modal, user can search for a person and results are displayed in a paged table. The user can then select one of the results to close the modal and to assign the selected person to the Contract.

This works, but then I want to handle browser history

So the state for the Contract form is defined as followed:

$stateProvider.state('detail.item.edit', {
url: '/edit',
views: {
'@detail': {
templateUrl: 'myModule/views/contract.detail.item.edit.html',
controller: 'contracts.detail.item.ctrl'
}
}

I can't figure out how to add a state for the modal. Plus it is opened from a directive, which makes things trickier. When the modal is closed, the Contract state should not be reloaded either.

Has anyone attempted doing that already ?

@ksperling
Copy link
Contributor

The states for the dialog would need to be children of the detail.item.edit state.

Is the dialog used from multiple different states or just from the one? In that case you'd need separate copies of the dialog's own states attached to each of the states from which it can be used. This is not something that's supported out of the box at the moment, but you can write your own helper function that takes $stateProvider and the state to attach to and then generates the dialog states underneath that state.

It's hard to see how you'd do all this purely from a directive, or how it would even interact with the directive.

To support that sort of thing the whole state transition logic would effectively have to trickle down the scope tree, with each layer handling parts of the parameters or URLs, but there would be a large number of challenges with such an approach.

@graphicsxp
Copy link
Author

eventually, the dialog will be called from different states. If I follow you right, I'll have to add the dialog states as children of the caller's state, leading to duplicated states across the application if I don't use the method you've described. For now, I'd say it's not an issue. Can't you use the same templateUrl for different named states ? I guess I'll just have to name the dialog states differently according to what parent they are attached to.

It'd be great if that scenario was supported by the ui-router plugin, as dialog modals are widely used.

@ksperling
Copy link
Contributor

@graphicsxp yes, that's right, all the dialog's states would be inside the caller's state, and while the user is navigating through the dialog the caller state would remain active.

It seems cases like this could benefit from having a way to define a "blueprint" for a re-usable sub-tree of states, which can then be "instantiated" in multiple places in the state tree. Maybe we could call it a "component" instead of a blueprint, and allow them be bound to a view in a state, along the lines of

$stateProvider
  .component('contactsearch', {
    template: ...,
    controller: ...,
  })
  .state('contactsearch.step1', {
    url: '/step1/:param',
    ...
  })
  .state('contactsearch.step2', {
    ...
  })


$stateProvider.state('abc', {
  url: '/abc/:abcParams1
  views: {
    'details': {
      component: 'contactsearch'
    }
  }
});

The root state of the component would effectively need to be merged into the calling state, and child states of the component become children of the calling state (if we'd want to allow multiple components to be attached to the same state we're going to have to look at what's effectively "orthogonal regions" in the state machine, but then this is a feature that's been hinted at a few times)

All URLs within a component would need to be relative, so that they can be combined with the URL for any parent state. Because the root state of the component gets merged into the caller, it can't have it's own URL (or state parameters), but it's children could. We'd probably have to do some magic so a component gets it's own separate namespace for parameters and it's own $stateParams.

In fact if we do allow multiple "orthogonal" components, the calling state might have to explicitly provide a URL parameter that the entire URL of anything that happens within the component's own state machine gets serialized into, e.g. the calling state's URL pattern might look like '/abc/{abcParameter}/?details' if using a query parameter, or '/abc/{abcParameter}/{details:.*}' with a path parameter -- with the convention being that the parameter name matches the name of the view that the component is instantiated into. In most cases there would have to be some encoding/escaping (e.g. percent-encoding) applied to the component's URL, depending on how it's inserted into the parent URL. (I think I'll stop here... there's a lot more detail that would need to happen)...

@jeme what are your thoughts on this?

@ggoodman
Copy link

This component idea is exactly what I'm looking for. Big +1.

I've had to embed my 'component' as an iframe right now, but that causes all kinds of problems.

@laurelnaiad
Copy link

I'd like to have something like this. In the back of my mind I've been thinking about how to load components that are defined in modules that aren't depended upon by the app module. It might take the form of having the components implement a specific interface so that the app module wouldn't need to know the particulars, and using some sort of div that is defined outside of the application but is absolutely-positioned into place within the app, along with some message busing between the two. I wonder how this might fit into that idea, if at all.

@skrivanos
Copy link

A more basic question about modal dialogs; has anyone successfully been able to use ui-router together with ui-bootstrap's modals/dialogs? If so, are there any Plunkr examples somewhere?

@jeme
Copy link
Contributor

jeme commented May 16, 2013

@stu-salsbury If you find a way to load modules and activating them (e.g. if they include things like directives) without having to depend on them directly, I am very interested to hear about your findings, because we are about to develop a prototype where we will allow for deployment of add-ins, each add-in would be in a module by it self naturally, and so far we figured that we had to dynamically add those to the main module on page load.

@ksperling to be honest, for "generic states" that are to be fitted under existing states, I have been satisfied with #95 as a workaround solution that was ok... But that fits into cases where it is natural that even if they are common states (e.g. Edit, Delete, Create for different type of records)... They are still essentially different at least from a conceptual standpoint... store.customer.edit might be similar to store.employee.edit, but they aren't conceptually the same, so the "full state name" that differs makes sense to me here.

However, general error dialog etc... here the idea of having store.customer.error and store.employee.error doesn't make as much sense to me, so the above approach crumbles at that.

In any case, I would be in favor of separating this out into maybe a $stateComponentProvider or just $componentProvider, otherwise I fear that the $stateProvider will end up growing enormously big... you mention your self to re-factor certain things out...

And since you would probably not define your components along side of your states, I think that would be ok to do.

@timkindberg
Copy link
Contributor

@jeme @ksperling I like this idea of "components"! +1 It's like there are several big ticket features that we are seeing users ask for and that I think will ultimately be needed to consider our router the ultimate solution. The ones that come to mind now are serialization of state to params and this component feature.

@jeme, I believe you are using the word "properly" when you mean "probably". I've seen it several times, so I just wanted to mention it.

@ggoodman
Copy link

@timkindberg I would like to reiterate my support for components and say that this could be very complementary to the idea of providing a decoupled state serialization approach.

The architecture could be a series of named, but independent root state trees, wherein each state tree can provide its own state serialization / decoding methods (aka: router). The behaviour of the current code would be replicated by having a default, unnamed base state tree that uses the corrent url-based serialization / decoding.

I think that this approach would allow custom serializer / decoder services to provide the kind of modular 'blueprint' you mentioned above and be able to tack that on at specified states.

$stateProvider.module("reusableCRUD", {
  router: $subordinateToStatesRouter(['packages.item', 'widgets.detail.item'])
})
.state('create', ...)
.state('update', ...)
.state('delete', ...);

Perhaps I'm dreaming in technicolor though ;-)

@laurelnaiad
Copy link

@jeme, I don't think there is a way to load an addin into the module after module definition. The solution I have in mind is to make the parent app depend on something you might call the "compoponentManager" module and the add-in apps depend on something you might call the "component" module, and then using jquery to keep them positionally cohesive and using eventing (through a dom node or nodes that is/are in neither the app module nor the add-in modules. So not too clean. However, hopefully it would be performant (enough) and easy (enough) to code to once it's available. It's not on my short term list, though.

Because such add-ins would want to participate in routing, I mentioned it here.

@nateabele
Copy link
Contributor

I've done this. I ended up triggering the dialog in onEnter, and I actually think I found a bug in the modal directive because of it (the background doesn't disappear when it closes).

I'll try to condense it to an example tomorrow.

@nateabele
Copy link
Contributor

On the 'components' front, I've done something where I provide my application's base module with a list of all potential dependencies that various different controllers used, and it would iterate through those, checking for the existence of each one in a try/catch block, adding them to the base module's dependency array if they existed.

That way, if your dependencies vary by page or whatever, at least you can always load in the ones you need based on which other scripts you load. The main trick is, obviously, you need to know all possible dependencies up-front.

My developers and I are taking a week off at the end of may to do some Open Source work we've been meaning to get to. I can see about adding hooks to the module API if there's sufficient interest.

@jeme
Copy link
Contributor

jeme commented May 17, 2013

@timkindberg

@jeme, I believe you are using the word "properly" when you mean "probably". I've seen it several times, so I just wanted to mention it.

Y... I hate those two words... English being my second language, i switch similar words around sometimes... >.<

@ksperling
Copy link
Contributor

It's hard to see how you'd do truly dynamic (i.e. after the app is already initialized) loading of angular modules, given how $injector works. E.g. a new module you load could try to $provide.decorate() a service in a module that's been loaded earlier and has already been instantiated, which wouldn't be possible because you've already handed out references to the un-decorated instance to the code that's already running.

It seems to me you'd have to have some kind of setup where each dynamically loaded "sub-system" (I'm avoiding the word "module" on purpose) gets it's own injector and has some way to access a parent/global injector to access services like an event bus that allows the sub-systems to communicate. How you mix together the views from the different sub-systems is yet another question though.

@nateabele
Copy link
Contributor

@ksperling It can be done, we have the technology. ;-) https://gist.github.com/nateabele/5610305

@jeme
Copy link
Contributor

jeme commented May 20, 2013

@nateabele Thats just Lazy loading of modules/components isn't it? Where you still know the components/modules up front as you register them in angular.autoLoad...

We are (at least some of us) talking more in a plugin model where the components/modules you depend on change over the cause of the applications lifetime... Obviously this is still doable if you just inject the "config object"... But that means that deployment of a new component also means reload of the browser.

I am perfectly ok with that, and when it comes to a real plugin model, I think there is more important things I need to figure out before that one reload of the browser becomes something I wish to solve (if even possible)... After all...

@nateabele
Copy link
Contributor

@nateabele Thats just Lazy loading of modules/components isn't it? Where you still know the components/modules up front as you register them in angular.autoLoad...

@jeme That's correct. I should have clarified, this is only a first pass at it. However, autoLoad() can be called multiple times, and the value could be based on, for example, a static JSON file that gets periodically reloaded from the server.

@laurelnaiad
Copy link

@nateabele -- are you saying that autoLoad will let you load a new module as a dependency at runtime? From the followin comment in the code I assumed it wasn't possible:

Angular must be boostrapped manually (http://docs.angularjs.org/guide/bootstrap) *after*
 * your autoload configuration is initialized, and *before* your application modules are loaded.

I'd be really excited to be able to add dependencies on the fly/lazily. I just don't know how to interpret your comment about periodically reloading from JSON...

@nateabele
Copy link
Contributor

@stu-salsbury Sort of. This is just a first step. All it really proves is that you can get Angular to load a module's dependencies dynamically, instead of throwing an exception and bailing out. Now, automatic module loading is not the same thing as lazy module loading. Obviously, this would be the next step.

The trick is, Angular makes it difficult to 'know' what module a particular controller/service/whatever is supposed to come from, since all resources exposed by a module get thrown into a single app-wide 'namespace'. If it was possible to know up-front what module each of a controller's dependencies was defined in, you could theoretically defer execution until they're all loaded, but again, unless you know up-front where everything is defined, you have to load everything before you can do anything.

The only way around this I can think of is extending injector annotations with extra syntax, such as:

/* ... */
.controller("ItemsController", [
  "moduleA.serviceB", "moduleB.serviceA", function(serviceB, serviceA) {
    /* ... */
  }
]);

We're getting a little off-track here, but if anybody has any thoughts or would like to help work on this, find me in #angularjs on Freenode.

@graphicsxp I posted my example code for a state-based modal here: #133 (comment)

@ksperling
Copy link
Contributor

@nateabele maybe an interesting idea would be to generate a 'map' of exported services / controllers of each module at build time into a JSON format of some sort, so that initially you'd only have to load that metadata rather than the actual code.

@nateabele
Copy link
Contributor

@ksperling Yeah, that was my other thought, but I'm not as familiar with hacking $injector. I imagine you'd need some way to defer it so that dependency modules could be loaded?

@ksperling
Copy link
Contributor

Well $injector is generally lazy, so as long as you don't use anything that depends on anything that you want to load lazily you should be fine. Anything that wants to talk to a provider or participate in configuration would have to be eager though

@nateabele
Copy link
Contributor

Right, the cases I'm thinking of are:

  • You have states representing a sub-section of an app, and those states use controllers in a lazy-loaded module (I suppose this could be done in ui.state itself, without involving $injector)
  • You have controllers using services that are in a lazy-loaded module

That's the big one, IMO. The other solution that @stu-salsbury came up with was allowing states to have i.e. a depends key, which lists dependency modules, and ui.state could ensure they're loaded as a pre-resolve step. However, that would require ui.state to manage a mapping between modules/script URLs (until/unless Angular core does), which feels a bit out-of-scope.

@nateabele
Copy link
Contributor

Closing this, as the original issue has been resolved. I've created a new issue where we can continue the discussion on module lazy-loading: #146.

@zeves095
Copy link

zeves095 commented Mar 2, 2017

i have one idea:
state('a.a',{
params: {stage:'a'}
view: {template: ... , controller: ...}
})
. state('a.b',{
params: {stage:'b'}
view: {template: ... , controller: ...}
})
...

in the controller you can define status of modal (open / closed) and pass params to it; i.e. stage: 'a' , 'b' ...

And if it is derictive - you can just pass paramter to it from controller

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants