Angular UI路由器 - 无父子状态

时间:2014-08-05 14:49:27

标签: angularjs angular-ui-router

认为我的目标不能与AngularUI路由器一起工作 - 但我会把它放在这里以防万一有人可以证明我错了或有替代解决方案或解决同样问题的解决方法。

目标

我想显示更改网址的模态窗口,但可以从我的应用程序中的任何位置打开 - 无论哪个父状态当前处于活动状态。具体来说,我希望更改URL,以便在按下浏览器/设备后退按钮时,关闭模式(即应用程序将返回到他们使用的任何父状态)。用户可以在使用应用程序时随时打开这样的模式(例如,可以从应用程序的主菜单栏访问帮助窗口)。

真正想要做的是将模态状态复制并粘贴为每个可能的父状态的子项(即将帮助状态注册为每个用户配置文件的子项) /搜索结果/ home / etc ...)。如果应用程序中只有一个这样的模态,那么可能是一种可接受的方法 - 但是当你开始将几个全局可访问的模态子状态引入应用程序时,多个子状态注册开始变为真实问题

为了更清楚地说明,这是一个用户故事:

  1. 用户正在查看一些搜索结果(他们无限滚动浏览了几页的搜索结果)。
  2. 他们想要执行某项操作但他们不确定如何实现该操作,以便他们点击应用程序标题中的帮助图标。
  3. 将打开一个模式对话框,该对话框位于他们正在查看的搜索结果上方。
  4. 他们搜索帮助并弄清楚他们需要做什么。
  5. 他们按下设备的后退按钮。
  6. 模态对话框关闭,显示他们一直在查看的状态,而不会丢失任何上下文。
  7. 用户执行他们的任务并对自己非常满意 - 由于愚蠢的用户体验设计,在应用程序开发人员中生气。
  8. 在上面的故事中,我认为导致后向事件关闭模态的唯一方法是将模态绑定到AngularUI路由器的状态转换。用户将从搜索结果状态(url:/search-results)转到帮助状态(url:/search-results?help) - 但是,在另一种情况下,他们可能会从用户个人资料状态转到(url:{{ 1}})到帮助状态(url:/profile/123)。这里的关键是,帮助没有直接注册为两者搜索结果和配置文件状态的子项,但不知何故独立地作为一种孤立状态可能可能应用于任何父项

    替代目标

    这不是我首选的解决方案。如果可以在不更改URL的情况下使浏览器/设备返回按钮关闭模态,那么我可以使这些模态独立于AngularUI路由器工作 - 但我不喜欢这样的方法,这意味着对不同类型的观点采用不一致的开发方法(谁知道,也许在未来我们将决定其中一个模态窗口本身应该是一流的状态,这需要从一个改变接近另一个 - 这是不可取的)。我认为这是一种不可靠的方法,因为根据我的经验,处理后退事件并非易事。

    这实际上对许多情况都很有用(例如,用户可以点击返回以关闭子菜单或上下文菜单),我只是不认为它是技术上可行的解决方案 - 但是随意证明我错了。 ; - )

    备注

    1. 我知道it is possible to open modal child states - 事实上,我实现了这一点,其中子状态明确地绑定到特定的父状态。
    2. 这适用于专门针对移动设备作为其主要用例的应用。这意味着后退按钮是一个非常重要的考虑因素 - 移动用户使用后退按钮关闭或取消对话框的正常行为我明确地想要训练我的应用用户在他们已经习惯使用后退按钮时点击关闭。
    3. 对不起,我没有代码试图呈现 - 我不知道如何让这个工作甚至从哪里开始 - 我的研究都没有说明问题(也许我正在搜索错误的条款?)。
    4. 提前感谢您提供的任何帮助!

      修改 1.更新了用户故事说明,以便更清晰地包含具体的URL /状态示例。

2 个答案:

答案 0 :(得分:1)

嗯,对于有类似需求的人,我找到了一个简单的解决方案,它基本上超出了UI路由器的整个路由机制。

首先,我认为应该可以在即将发布的0.3版本中使用deferIntercept功能,详见in this SO answer。但是,我的解决方案采用了不同的方法。我没有使用查询参数来识别这些孤立的视图(即?help),而是使用url片段标识符(即#help)。这是有效的,因为路由机制似乎忽略了哈希符号之后的任何内容。

在我成功完成这项任务之前,我确实遇到了一些问题 - 特别是在$location服务中处理非html5模式时。据我了解,在片段标识符中包含哈希符号(即,网址不能包含两个#符号),技术上非法非法,因此它带来一些风险,但是根据我的测试,似乎浏览器不会抱怨太多。)

我的解决方案涉及拥有hashRouter服务,该服务管理将查询数据序列化和反序列化到片段标识符和从片段标识符反序列化的作业,并监视$locationChangeSuccess事件以处理URL中的外部更改(即,按下浏览器或设备的后退和前进按钮。)

这是我服务的简化版本:

hashRouter.$inject = [
    '$rootScope',
    '$location'
];

function hashRouter($rootScope, $location) {
    var service = this,
        hashString = $location.hash(),
        hash = fromHashString(hashString);

    $rootScope.$on('$locationChangeSuccess', function (e, newUrl) {
        var newHashString = getHashSection(newUrl);
        if (newHashString != hashString) {
            var newHash = fromHashString(newHashString);
            service.hash(newHash.name, newHash.params);
        }
    });

    service.hash = function (name, params) {
        var oldHash = hash,
            oldHashString = hashString;

        hash = { name: name || '', params: params || {} };
        hashString = toHashString(hash);

        if (hashString !== oldHashString) {
            var oldHashExists = oldHashString.length > 0;
            if (oldHashExists) {
                $rootScope.$broadcast('hashRouteRemoved', oldHash);
            }

            if (hashString.length > 0) {
                $rootScope.$broadcast('hashRouteAdded', hash);
            }

            $location.hash(hashString);

            if (oldHashExists) {
                $location.replace();
            }
        }
    };

    return service;

    function toHashString(data) {
        var newHashString = '';

        var name = data.name;
        if (!!name) {
            newHashString += encodeURIComponent(name);
        }

        var params = data.params;
        if (!!params) {
            var paramList = [];

            for (var prop in params) {
                var key = encodeURIComponent(prop),
                    value = params.hasOwnProperty(prop) ? encodeURIComponent(params[prop].toString()) : '';
                paramList.push(key + '=' + value);
            }

            if (paramList.length > 0) {
                newHashString += ':' + paramList.join('&');
            }
        }

        return newHashString;
    }

    function fromHashString(urlHash) {
        var parsedHash = {
            name: '',
            params: {}
        };

        if (!!urlHash && urlHash.length > 0) {
            if (urlHash.indexOf(':') !== -1) {
                var hashSegments = urlHash.split(':');
                parsedHash.name = decodeURIComponent(hashSegments[0]);

                var querySegments = hashSegments[1].split('&');
                for (var i = 0; i < querySegments.length; i++) {
                    var pair = querySegments[i].split('=');
                    parsedHash.params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]) || null;
                }

            } else {
                parsedHash.name = decodeURIComponent(urlHash);
            }
        }

        return parsedHash;
    }

    function getHashSection(url) {
        if (url.indexOf('#') === -1 || (url.indexOf('#!') !== -1 && url.indexOf('#') === url.lastIndexOf('#'))) {
            return '';
        }

        var urlSegments = url.split('#');

        return urlSegments[urlSegments.length - 1];
    }
}

angular.module('myApp').service('hashRouter', hashRouter);

有关服务的注意事项有几点:

  1. 我推出了自己的序列化/反序列化功能,除了完整之外,它们都是完整的,所以请自行承担风险 - 或者用更合适的东西替换。
  2. 这取决于在不使用html5模式时使用hash-bang(#!而不是#)的 bang 部分。
  3. 如果您对序列化/反序列化功能感到困惑,请非常小心:我在一些无限循环场景中发现自己,这基本上会导致我的浏览器崩溃。所以一定要彻底测试!
  4. 每当您打开/关闭使用该服务的对话框/菜单/等时,您仍需要调用该服务,并酌情收听hashRouteAddedhashRouteRemoved事件。
  5. 我已经构建了这个系统,一次只支持一个视图 - 如果你需要多个视图,那么你需要稍微自定义代码(虽然我猜它可以很容易支持嵌套视图)。
  6. 希望如果其他人需要和我在这里做的一样,这可以节省一些时间: - )

答案 1 :(得分:0)

您是否可以使用单个州作为应用中所有内容的父级?我在角度应用程序中做同样的事情。

$stateProvider
            //root route
            .state('app', {
                url: '/',
                templateUrl: '/scripts/app/app/views/app.html',
                controller: 'appController',
                resolve: {
                    //resolve any app wide data here
                }
            });

然后你可以将你的模态作为这个状态的孩子。这样您就可以始终转换回此路线以返回到应用的默认状态(当您的模态关闭时)。 以这种方式执行操作的另一个好处是,您可以使用此路由的视图作为布局,以放置任何不会在页面之间更改的标记(标题,侧边栏等)。