erikringsmuth / app-router

Router for Web Components
https://erikringsmuth.github.io/app-router/
MIT License
611 stars 83 forks source link

Data-binding usage with Polymer ready() #28

Closed ghost closed 9 years ago

ghost commented 9 years ago

I am quite unsure whether this is a problem with Polymer or app-router, so this should be considered more of a help request than a bug filing.

I need to determine the value of an attribute set through data-binding when my element is ready. This works fine when I freshly load the page. However, when I visit the page through a link or router.go(), the value will always be empty.

What could possibly cause this?

erikringsmuth commented 9 years ago

Do you have any code I could look at?

Are you trying to achieve something like this http://polymer-app-router.herokuapp.com/demo/1337?queryParam1=Routing%20with%20Web%20Components! or is it different?

link https://github.com/erikringsmuth/polymer-router-demos/blob/master/app-router/layouts/sidebar-layout.html#L15

route https://github.com/erikringsmuth/polymer-router-demos/blob/master/app-router/index.html#L15

page https://github.com/erikringsmuth/polymer-router-demos/blob/master/app-router/pages/demo-page.html#L4-L12

semateos commented 9 years ago

I'm also struggling with some data binding issues. I want to bind values between the view that contains the router and a routed page. The use case is something like a user object that's shared between several views. I tried a bunch of things, including the Node.bind() function example below. Any thoughts on how to create these kind of bindings? Here's a rough example that doesn't work. Link to my full code below.

<polymer-element name="test-app" attributes="user">

  <template>

  <app-router id="router">
      <app-route path="/" import="../pages/posts.html" 
        on-activate-route-end="{{onRouteActivate}}"></app-route>
      <app-route path="/login" import="../pages/login.html" 
        on-activate-route-end="{{onRouteActivate}}"></app-route>
    </app-router>
  </template>

  <script>

    Polymer('stimpy-app', {

      observe: {
        user: 'userUpdated'
      },

      userUpdated: function(){
        this.$.router.go('/');
      },

      onRouteActivate: function(e){
        var activeElement = this.$.router.activeRouteContent.firstChild;
        activeElement.bind('user', new PathObserver(this.user));
      }
    });

  </script>
</polymer-element>

Both posts and login would contain an element that exposes user on the attributes.

My code here: https://github.com/semateos/stimpy-polymer-login

There are instructions for installing it on the readme.

Any thoughts very welcome. Thanks!

erikringsmuth commented 9 years ago

I see what you're getting at. You want to bind values outside of the router to the dynamically created routes.

@semateos Is there a default user to log into your web app for debugging?

semateos commented 9 years ago

User: john-doe Pass: john-doe-password On Oct 10, 2014 7:19 PM, "Erik Ringsmuth" notifications@github.com wrote:

I see what you're getting at. You want to bind values outside of the router to the dynamically created routes.

@semateos https://github.com/semateos Is there a default user to log into your web app for debugging?

Reply to this email directly or view it on GitHub https://github.com/erikringsmuth/app-router/issues/28#issuecomment-58727782 .

erikringsmuth commented 9 years ago

@semateos I get a Login Failed error with that username and password. Is there some initial database setup when running locally?

semateos commented 9 years ago

Huh, it's using a file-based db for testing, so no install required. And, it's using an OAuth server for login. I just tested it again, works for me. It should work for you because the oauth token is in the codebase and it's not tied to a domain. I've got a demo running here (with the broken data binding, so it's a little funky): http://quiet-mesa-6846.herokuapp.com/ Normally the login would redirect to the posts view after successful login, but right now it just sits there because the app doesn't observe the change to user.

semateos commented 9 years ago

I'm thinking it may be a timing issue. I'm getting this in console: "Attributes on stimpy-posts were data bound prior to Polymer upgrading the element. This may result in incorrect binding types." I thinking I might be able to wait for the element onReady event and then add the binding...

erikringsmuth commented 9 years ago

OK, so I think you've pretty much got it.

The first thing I did was put together a simple page to make sure this worked. https://github.com/erikringsmuth/outer-bind-demo

Every time a route loads the sessionId is bound to the page. It's binding an external variable exactly how you did it so it verifies that it works. https://github.com/erikringsmuth/outer-bind-demo/blob/master/index.html#L17

Now for your example, you have two stimpy-login.html pages. This threw me for a loop because I couldn't figure out why the one I was editing wasn't showing the changes in the browser.

This page looks correct, but isn't the one being loaded. https://github.com/semateos/stimpy-polymer-login/blob/master/public/components/stimpy-login.html#L227

This page is being loaded but doesn't do anything with the user. https://github.com/semateos/stimpy-polymer-login/blob/master/public/pages/stimpy-login.html#L225

Over here you're observing user changes on the app-router element, but nothing else in the app sets attributes on the app-router. https://github.com/semateos/stimpy-polymer-login/blob/master/public/components/stimpy-app.html#L102

I think if you add this.$.router.go('/login'); to your actual stimpy-login page everything will work.

One alternative could also be to make an element like this <stimpy-user-service user="{{user}}">. Then have this element make the ajax call to load the user and set the user attribute. Then every page that needed user info could include this element inside the page to bind to the user. Just an idea...

semateos commented 9 years ago

Yeah, sorry about the duplicate file names - I moved those into public/pages and didn't remove the old ones in components.

I'll try your demo. Did you test if two-way binding works? I've made some progress with getting the data into the loaded view, but in my demo changes to the bound value in the child view are not reflected in the parent element...

erikringsmuth commented 9 years ago

One thing I noticed here is that you're creating a PathOvserver on stimpy-app itself. https://github.com/semateos/stimpy-polymer-login/blob/master/public/components/stimpy-app.html#L114

I think you want to bind to the activeElement like this this.bind('user', new PathObserver(activeElement.user)); where this is the stimpy-app. In theory that should bind the two together, although I tried it and it isn't picking up the change from stimpy-login. I'm not sure what's going on there.

erikringsmuth commented 9 years ago

two-way data binding doesn't seem to work in my demo, but I can wire it up so that the child view sets the value and the parent listens.

child https://github.com/erikringsmuth/outer-bind-demo/blob/master/pages/home-page.html#L19

parent https://github.com/erikringsmuth/outer-bind-demo/blob/master/index.html#L29

It looks super weird though!

semateos commented 9 years ago

Here's another test: https://github.com/semateos/outer-bind-demo

I tried to bind in both directions.

If you uncomment this line it does indeed seem to bind both ways (sort of): https://github.com/semateos/outer-bind-demo/blob/master/index.html#L29

If you view the console you'll see the parent observing the change to the bound value as it changes in the child.

But changing views kills the bound data. Ideally we want the data to persist between all views that have it bound.

I think I'll ask one of the google polymer folks to weigh in and see if they have an opinion.

erikringsmuth commented 9 years ago

This is the ping-pong of ideas!

Here's my new demo-app https://github.com/erikringsmuth/outer-bind-demo/blob/master/index.html#L27-L33

and the child https://github.com/erikringsmuth/outer-bind-demo/blob/master/pages/home-page.html#L14

Changing the value in the child will change it in the parent and persist it across page changes. It won't persist on a page refresh since we aren't really persisting it.

erikringsmuth commented 9 years ago

And oh boy did i screw up that comment in there. First I set the child's value to the current parent's value. Then I bind the parent's value to the child's value that I just set. This means changes to the child will reflect in the parent. Setting the child's value before binding makes sure the parent doesn't bind to an undefined attribute.

semateos commented 9 years ago

Cool! It's very very close. It seems to bind perfectly between the views. I put in editable fields in both home page and demo page and they stay in sync - and they both update the parent value. But, updating the value in the parent view does not sync it to the children until the view is refreshed.

See: https://github.com/semateos/outer-bind-demo/blob/master/index.html#L42

So close!

semateos commented 9 years ago

You could just manually set it on the child again at that point... but it feels like I'm not understanding the bind function correctly. It seems like there should be a way to set up proper two-way binding like everything else in polymer has naturally.

erikringsmuth commented 9 years ago

Yet another idea. Seems like overkill, but this looks like it does it. https://github.com/erikringsmuth/outer-bind-demo/blob/master/index.html#L27-L29

semateos commented 9 years ago

Sweet! Works! Does seem like overkill - but perhaps that is actually how two way binding is set up. Any interest in building it into the component? Thinking something like:

<app-router id="router" bind="sessionId">

Thought that might not be exactly what one would want. I could take a whack at it if you like.

semateos commented 9 years ago

Arg. Still have issues with more complex data. Looks like a bound object gets converted to a string at some point. I think it's a timing and element upgrading thing related to that warning - it sounds like exactly what it's warning about:

sessionChange {id: "123"}
(index):27 routeActivated
polymer.concat.js:9049 Attributes on home-page were data bound prior to Polymer upgrading the element. This may result in incorrect binding types.
(index):33 sessionChange Object {id: "321"}
(index):33 sessionChange [object Object]

My object binding test: https://github.com/semateos/outer-bind-demo

erikringsmuth commented 9 years ago

Polymer seems to give that "data bound prior to Polymer upgrading the element" error a lot and I'm not sure what it means.

I put together another concept of the router with a sessionId and a login page. It's on the state-element branch. https://github.com/erikringsmuth/outer-bind-demo/tree/state-element

If the session ID isn't set it gets redirected to a login page. Once there is a sessionId it will route as long as you don't sign out. Deleting the sessionId effectively signs you out.

The app-state element holds a static reference to the state. That way every element that imports app-state will get the same state.

Polymer needs to figure out a good solution to services. This is the one area where HTML elements fall short. The static state element is a work-around but still not great IMO.

erikringsmuth commented 9 years ago

FYI, this is Polymer's suggested way of passing variables between multiple elements.

https://www.polymer-project.org/docs/polymer/polymer.html#global

semateos commented 9 years ago

OK, cool, thanks! That does look like it works well. I had seen that but I didn't understand how that kind of global could maintain a single state when it's being included as a unique element in each view. It works, but I don't fully understand why. I've also made some progress on lazy loading elements and binding them. Might be useful to you in the router or to others trying to lazy load and then data bind. See: https://github.com/semateos/polymer-lazy-load

erikringsmuth commented 9 years ago

I'm not going to add data binding from outside of the router just yet. There are a few issues there. One being that Node.bind() is Polymer and not native code. Currently the app-router doesn't have any external dependencies and can work with other libraries.

If we did have a syntax like this.

<app-router id="router" bind="sessionId otherProperty">

You wouldn't know what value sessionId has. You'd need something like this.

<app-router id="router" sessionId="{{sessionId}}" otherProperty="{{otherProperty}}">

Then bind every attribute on the router to every view inside of the router. It'd get messy quick.

My suggested method for now would be a state object, although this doesn't seem great either.

Here's a slightly more in-depth explanation of a global state element. https://github.com/erikringsmuth/outer-bind-demo/blob/state-element/elements/app-state.html#L6

  1. HTML Imports de-dupe imports. This means that app-state.html only gets loaded once even if it's imported by multiple elements. It gets imported and immediately registers an app-state element that can be used anywhere after that.
  2. Everything in the <script> tag is run immediately and ultimately calls the native document.registerElement('app-state'). The second argument you pass to Polymer('app-state', {}) is the prototype for every app-state element created.
  3. When you create a new instance of app-state it's state refers to the state_ object which was only defined once when you first registered the element. If we had put state inside of a Polymer ready() or created() function then it would be a separate state per instance.

I think the Polymer team is working right now on other ways of passing values around between elements. I'd like to see what they come up with over the next couple months. If they can come up with something equivalent to Angular's services, that'd be the best solution altogether.

semateos commented 9 years ago

Yeah, agree. The global state element works very well. I understand it now. My next holy grail is routing that works out of the box in cordova as well as on the web. But that's for another day.

erikringsmuth commented 9 years ago

@sprottenwels, let me know if this thread helped at all or if it completely missed what you were asking.

OS-Chris commented 9 years ago

I've tried the recommended globals approach and while I got it to work between components, I couldn't get it working with app-router (any redirect would cause the state to be reset).

It's messy I know but my workaround is using the window namespace (i.e. window.MyApp = {user: {sessionId:1}}) to store app globals. I then use core-signals to pass events (i.e. login/logout) around to other components.

erikringsmuth commented 9 years ago

I'm adding this feature as a follow up to another PR https://github.com/erikringsmuth/app-router/issues/45. I'm hoping to get 2.1.0 released this weekend. This might solve this issue.

erikringsmuth commented 9 years ago

Hey @sprottenwels and @semateos, I got version 2.1.0 released with a few new features.

The new before-data-binding event and the example at the bottom here might be what you're looking for https://erikringsmuth.github.io/app-router/#/events#before-data-binding.

There are a few other new data binding features in 2.1.0 as well https://erikringsmuth.github.io/app-router/#/databinding/1337?queryParam1=Routing%20with%20Web%20Components!.

erikringsmuth commented 9 years ago

I think this has been resolved with the latest changes. Feel free to open it back up or create a new issue of you have more questions.