dsuryd / dotNetify

Simple, lightweight, yet powerful way to build real-time web apps.
https://dotnetify.net
Other
1.17k stars 164 forks source link

Unable to get deep-linking working per Routing docs #133

Closed bugged84 closed 5 years ago

bugged84 commented 5 years ago

@dsuryd I'm having trouble getting deep-linking working as described in the API Reference for routing.

I've forked your repo and added a group chat demo here.

Here is the App view model which owns the high level routes for the site.

public class App : BaseVM, IRoutable
{
  public class Link
  {
     public string Caption { get; set; }
     public string Id => Route.TemplateId;
     public Route Route { get; set; }
  }

  public RoutingState RoutingState { get; set; }

  public List<Link> Links =>
     new List<Link>
     {
        new Link
        {
           Route = this.GetRoute("ChatRoomIndex")
         , Caption = "Chat Rooms"
        }
     };

  public App()
  {
     this.RegisterRoutes(
        ""
      , new List<RouteTemplate>
        {
           new RouteTemplate("Home")
           {
              UrlPattern = ""
            , ViewUrl = "ChatRoomIndex"
           }
         , new RouteTemplate("ChatRoomIndex")
           {
              VMType = typeof(ChatRoomIndexVM)
           }
        }
     );
  }
}

Here is the ChatRoomIndexVM which owns generated routes for each chat room.

public class ChatRoomIndexVM : BaseVM, IRoutable
{
  public RoutingState RoutingState { get; set; }

  public IEnumerable<object> ChatRooms { get; set; }

  public ChatRoomIndexVM()
  {
     this.RegisterRoutes(
        "ChatRoomIndex"
      , new List<RouteTemplate>
        {
           new RouteTemplate("ChatRoom")
           {
              UrlPattern = "(/:id)"
            , VMType = typeof(ChatRoomVM)
           }
        }
     );

     ChatRooms = new List<object>
     {
        new {Id = 1, Route = this.GetRoute("ChatRoom", "1")}
      , new {Id = 2, Route = this.GetRoute("ChatRoom", "2")}
      , new {Id = 3, Route = this.GetRoute("ChatRoom", "3")}
      , new {Id = 4, Route = this.GetRoute("ChatRoom", "4")}
      , new {Id = 5, Route = this.GetRoute("ChatRoom", "5")}
     };
  }
}

Here is the ChatRoomVM which is what I want to deep-link to by pasting a route such as <server>/ChatRoomIndex/5 into my browser.

public class ChatRoomVM : MulticastVM, IRoutable
{
  private readonly IHubCallerContextAccessor m_hubCallerContextAccessor;

  public string Name = DateTime.Now.ToLongTimeString();

  public RoutingState RoutingState { get; set; }

  public override string GroupName =>
     m_hubCallerContextAccessor
       .CallerContext
       .GetViewModelGroupName();

  public ChatRoomVM(IHubCallerContextAccessor hubCallerContextAccessor)
  {
     m_hubCallerContextAccessor = hubCallerContextAccessor
        ?? throw new ArgumentNullException(
           nameof(hubCallerContextAccessor)
        );

     this.OnRouted(
        (sender, e) =>
        {
           // handle user entry
        }
     );
  }
}

Currently navigating to a chat room using links from the home page works fine, but if I then copy the URL from the address bar to another tab, for example, then I get the following error in the client console.

Uncaught SyntaxError: Unexpected token <

Any ideas? Feel free to download my demo and see for yourself. It should work out of the box as it was created from your SPA demo.

dsuryd commented 5 years ago

Change the script tag src path for main.js in index.html to relative path from root:

<script src="/dist/main.js" charset="UTF-8"></script>
bugged84 commented 5 years ago

@dsuryd prepending the extra / made pasting URLs work. Thanks for finding that.

However, now that it's working, I am seeing another console error in the client that only appears when deep-linking, but not when clicking through the links in the UI.

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
    in ChatRoomIndex

I can create a separate issue if you like, but it seems it might be related to the original topic.

dsuryd commented 5 years ago

Can you find out where does the warning originate from?

bugged84 commented 5 years ago

Assuming you mean the full stack trace.

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
    in ChatRoomIndex
warningWithoutStack @ react-dom.development.js:512
warnAboutUpdateOnUnmounted @ react-dom.development.js:15630
scheduleWork @ react-dom.development.js:16927
enqueueSetState @ react-dom.development.js:11623
Component.setState @ react.development.js:413
setState @ dotnetify-react.js:195
setState @ dotnetify-react.js:2145
State @ dotnetify-react.js:2149
dispatchActiveRoutingState @ dotnetify-react.js:1087
(anonymous) @ dotnetify-react.js:1341
createViewFunc @ dotnetify-react.js:1627
loadReactView @ dotnetify-react.js:1630
loadView @ dotnetify-react.js:1576
routeTo @ dotnetify-react.js:1339
(anonymous) @ dotnetify-react.js:1166
(anonymous) @ dotnetify-react.js:1943
run @ dotnetify-react.js:1867
dispatch @ dotnetify-react.js:1766
pushState @ dotnetify-react.js:1679
pushState @ dotnetify-react.js:1976
(anonymous) @ dotnetify-react.js:1102
setTimeout (async)
handleRoute @ dotnetify-react.js:1101
iVM.$handleRoute @ dotnetify-react.js:2064
handleClick @ dotnetify-react.js:698
callCallback @ react-dom.development.js:145
invokeGuardedCallbackDev @ react-dom.development.js:195
invokeGuardedCallback @ react-dom.development.js:248
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:262
executeDispatch @ react-dom.development.js:593
executeDispatchesInOrder @ react-dom.development.js:615
executeDispatchesAndRelease @ react-dom.development.js:713
executeDispatchesAndReleaseTopLevel @ react-dom.development.js:724
forEachAccumulated @ react-dom.development.js:694
runEventsInBatch @ react-dom.development.js:855
runExtractedEventsInBatch @ react-dom.development.js:864
handleTopLevel @ react-dom.development.js:4857
batchedUpdates$1 @ react-dom.development.js:17498
batchedUpdates @ react-dom.development.js:2189
dispatchEvent @ react-dom.development.js:4936
interactiveUpdates$1 @ react-dom.development.js:17553
interactiveUpdates @ react-dom.development.js:2208
dispatchInteractiveEvent @ react-dom.development.js:4913
dsuryd commented 5 years ago

You should not have the chat room rendered on the same DOM element ('#Content') as its index. I suggest you move the list of chat rooms to a new component ('Lobby'?) and add another route template for it, so the index can switch between the lobby and the chat room given the URL pattern.

bugged84 commented 5 years ago

@dsuryd I guess don't understand. The list of chat rooms is already in its own component. It just happens to be called ChatRoomIndex instead of Lobby.

I already have both the chat room and the lobby set in the index.js

Object.assign(window, { ChatRoomIndex, ChatRoom });

I changed the DOM element in question to Content2 just to test your fix, and yes that gets rid of the error from my previous comment. However, now when the link is clicked or pasted, the chat room component is rendered below the list instead of appearing to render in a separate page.

I understand that you're suggestion above is trying to accomplish having the app swap between the lobby/index component and the chat room component, but I'm not understanding the approach you described. It seems like what you're suggesting is how it's already configured.

Do you have time to provide an example of the change I should make?

dsuryd commented 5 years ago

You need another route template with an empty URL pattern so that the ChatRoomIndex can switch between the lobby and the chat room view. Since the ChatRoomIndex itself that renders the chat room list, it should control the list's visibility, and the lobby view should just render blank.

ChatRoomIndexVM.cs

...
     public ChatRoomIndexVM()
      {
         this.RegisterRoutes(
            "ChatRoomIndex"
          , new List<RouteTemplate>
            {
               new RouteTemplate("Lobby")
               {
                  UrlPattern = ""
               },         
...     

ChatRoomIndex.js

import React from 'react';
import dotnetify from 'dotnetify';
import { RouteLink } from 'dotnetify/dist/dotnetify-react';

class ChatRoomIndex extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      ChatRooms: [],
      lobby: true
    };

    this.vm = dotnetify.react.connect('ChatRoomIndexVM', this);

    this.vm.onRouteEnter = (path, template) => {
      console.log(path);
      console.log(template);
      template.Target = 'Content2';
      this.setState({ lobby: template.Id === 'Lobby' });
    };
  }

  componentWillUnmount() {
    this.vm.$destroy();
  }

  render() {
    const chatRooms = this.state.ChatRooms.map(chatRoom => (
      <RouteLink vm={this.vm} route={chatRoom.Route} key={chatRoom.Id}>
        <div>Chat Room {chatRoom.Id}</div>
      </RouteLink>
    ));

    return (
      <div>
        {this.state.lobby && <div>{chatRooms}</div>}
        <div id="Content2" />
      </div>
    );
  }
}

const Lobby = () => <div />;

export { Lobby };
export default ChatRoomIndex;

index.js

import ChatRoomIndex, { Lobby } from './ChatRoomIndex';
import ChatRoom from './ChatRoom';
...
Object.assign(window, { ChatRoomIndex, ChatRoom, Lobby });
bugged84 commented 5 years ago

Ah, yes I see what you mean now. Thanks for that example. I'll try that tonight and post back here with the results.

bugged84 commented 5 years ago

So those little tweaks worked out great.

Before we close this issue, could you clarify one more thing about route templates.

Consider this one:

new RouteTemplate("ChatRoomIndex")
{
     VMType = typeof(ChatRoomIndexVM)
}

I've seen the VMType property used in various samples I've found in other issues tracked in this repo, but the docs don't make any mention of it.

Is it only required if the view model name does not match the template id? In other words, if the route template above did not specify a VMType, would the template automatically look for a view model named ChatRoomIndexVM?

I understand that this is the behavior for the ViewUrl property of RouteTemplate. For example, since the template above does not specify a ViewUrl to some component, then the template automatically assumes a component named ChatRoomIndex to be set in the global window in index.js.

However, I don't understand the purpose of VMType then, since a view model does not seem related to routing, but rather the component routed to connects to a specified view model after the fact. So why would one ever specify a view model in the RouteTemplate?

bugged84 commented 5 years ago

@dsuryd also, I've pushed some changes to my demo that include your fixes as well as some refactorings I made to improve the component structure for the group chat demo in case you're interested.

dsuryd commented 5 years ago

VMType is only used for server-side routing / rendering. This was in the original documentation for Knockout, was taken out as this feature needs maintenance.

bugged84 commented 5 years ago

@dsuryd but the routing we have been discussing here is, in fact, server-side correct?

dsuryd commented 5 years ago

Actually, no. Yes, the routes are defined on the server-side VM, but it's the client-side Javascript that performs the routing; as opposed to say, ASP.NET routing that resolves and delivers the html page to the browser.

bugged84 commented 5 years ago

@dsuryd quick follow up question to this routing topic. Is it possible to pass React props from the App component to whatever is rendered in the div content?

// App.js

return (
      <div id="Content2" />
);
dsuryd commented 5 years ago

No, it's not supported.