decipherinc/angular-history

语言: JavaScript

git: https://github.com/decipherinc/angular-history

AngularJS的历史服务。撤消/重做,那种事情。与“后退”按钮无关,除非你......
A history service for AngularJS. Undo/redo, that sort of thing. Has nothing to do with the "back" button, unless you …
README.md (中文)

角历史

AngularJS的历史服务。撤消/重做,那种事情。什么都没有 使用“后退”按钮,除非你想要它。

当前版本

0.8.0

公平警告:在此项目为1.0.0之前,API可能会发生变化 并且可能会破裂。

安装

bower install angular-history

要求

  • AngularJS 1.1.x +
  • ngLazyBind(可选)

运行测试

克隆此repo并执行:

npm install

抓住依赖项。然后执行:

grunt test

运行测试。这将抓住凉亭的测试组, 并在端口8000上的本地服务器上针对QUnit运行它们。

API文档

API文档

演示

目前,有些是API文档。

用法

首先,在您的应用程序中包含decipher.history模块:

angular.module('myApp', ['decipher.history']);

接下来,您需要将History服务注入组件:

angular.module('myApp').controller('MyCtrl', function($scope, History) {
    // ...
});

或者,您可以获取ngLazyBind 模块,如果你想支持延迟绑定。如果你有这个,这将变得有用 比如说,一个<input type =“text”>字段,你不希望每次按键都被记录下来 在历史上。如果此模块存在,则将提供历史记录服务 额外的选择。

看表达

您需要为History服务提供一个表达式:

angular.module('myApp').controller('MyCtrl', function($scope, History) {
    $scope.foo = 'bar';
    History.watch('foo', $scope);
});

可选的第三个参数是描述,当a时将发出 item通过$ broadcast()存档,撤消,重做或恢复。这个 允许您附加到事件并对信息执行某些操作,例如弹出窗口  其中包含“撤消”按钮的提醒。该值根据内插进行插值  传递了$ scope对象。

如果您有ngLazyBind模块,则可以提供第四个参数 History.watch():

History.watch('foo', $scope, 'Foo changed to {{foo}}', {timeout: 500});

这告诉历史堆栈不要经常每500ms更新一次。这个 值默认为1秒。如果你想使用默认的延迟绑定,那么 只需传递一个空对象{}作为第四个参数。如果你没有 安装ngLazyBind模块后,将忽略此对象。

观察对象

每当您发出以下命令之一时,您将收到一个Watch 目的:

  • history.watch()
  • History.deepWatch()
  • History.batch()

这在事件的基础上提供了另一层功能 历史服务将播出。您可以使用这些可链接对象来运行 回调取决于所采取的操作类型:

var w = History.watch('foo')
  .addChangeHandler('myChangeHandler', function() {
    console.log('foo got changed');
  })
  .addUndoHandler('myUndoHandler', function() {
    console.log('undid foo.  this message will self-destruct');
    w.removeUndoHandler('myUndoHandler');
  });

一般建议是根据需要倾听全球事件 一般功能,并将这些Watch对象回调用于特定的 功能。

撤销/重做

一旦watch()ed,你可以撤消或重做对它的更改,如果是这样的话 表达式是可分配的。如果您将函数作为表达式传递,则可以 不撤消/重做,但您仍然可以访问历史堆栈。

无论如何,要撤消,执行:

History.undo('foo', $scope);

$ scope将使用最新版本的对象进行更新。您 可以自由地撤消(),因为表达式的值有变化 你看()编辑它 - 这是一个完整的历史堆栈。

此外,将发出一个事件。 $ rootScope将$ broadcast()a History.undone事件,包含以下信息:

  • 表达式:撤消的表达式
  • oldValue:表达式的值已更改。这是副本!
  • newValue:表达式更改为的值
  • description:您可能已经通过的可选描述
  • 范围:传递给undo()的范围

重做几乎与您期望的一样:

History.redo('foo', $scope);

只有当你以前没有做过某些事情时,这才有效。您可以 撤消多次,然后重做多次。重做后发出的事件是 History.redone和信息是一样的。

如果需要,请使用History.canUndo(exp,scope)和History.canRedo(exp,scope) 知道那些事情。

还原

您可以在watch()指令时恢复为原始值 发行:

History.revert('foo', $scope);

如果您正在查看History.history并知道您希望在堆栈中的哪个位置 go,传递第三个参数,你将恢复到的特定版本 堆栈:

History.revert('foo', $scope, 23);

...将直接恢复到第23次修订,没有问题。

此外,History.reverted事件将返回指针 你传递了它(默认为0)。

忘记

如果您想停止观看表达式的更改,请发出:

History.forget($scope, 'foo');

历史将被清除,手表将被删除。请注意,$ scope参数首先出现。如果省略第二个参数,则将销毁该范围的所有历史数据和监视。如果省略第一个参数,则会擦除整个历史记录服务。

幻想:深刻的观察

也许它可以使用不同的名称,但通常会出现您想要的情况 观察整个对象数组,以便更改任何对象 属性。观察整个阵列/对象是非常低效的 对于更改,您不一定知道哪些属性已更新。

“深表”如此:

$scope.foos = [
  {id: 1, name: 'winken'},
  {id: 2, name: 'blinken'},
  {id: 3, name: 'nod'}
];
History.deepWatch('f.name for f in foos', $scope,
  'Foo with ID {{f.id}} changed to {{f.name}}');

这也适用于对象:

$scope.foos = {
  '1': {name: 'fe'},
  '2': {name: 'fi'},
  '3': {name: 'fo'},
  '4': {name: 'fum'}
};
History.deepWatch('value.name for (key, value) in foos', $scope,
  'Foo with ID {{key}} changed its name to {{value.name}}');

现在,只要其中任何一个的名称发生变化,就会有历史记录 堆栈。

有两种方法可以解决这个问题。

听一个事件

第一个是收听History.archived事件:

History.deepWatch('f.name for f in foos', $scope,
  'Foo with ID {{f.id}} changed to {{f.name}}');

$scope.$on('History.archived', function(evt, data) {
  $scope.undo = function() {
    History.undo(data.expression, data.locals);
  };
});

$scope.foos[0].name = 'fuh';

因此,您可以在视图中绑定到undo(),它将撤消更改为 FOOS [0]。名称。

传递给事件处理程序的数据看起来类似于 如上所述的History.undone事件:

  • 表达式:已归档的表达式,本地的本地表达式
  • oldValue:表达式的值已更改。这是副本!
  • newValue:表达式更改为的值。
  • description:您可能已经通过的可选描述
  • locals:包含表达式值的范围。例如,我们之上 将使用name属性在本地中提供对象f。

使用处理程序

第二种方法是使用Watch对象内置的处理程序:

var w = History.deepWatch('f.name for f in foos', $scope,
  'Foo with ID {{f.id}} changed to {{f.name}}');

w.addChangeHandler('myChangeHandler', function($expression, $locals, foo) {
  $scope.undo = function() {
    console.log('undoing foo with name "' + foo.name + '"');
    History.undo($expression, $locals);
  });
}, {foo: 'f'});

$scope.foos[0].name = 'fuh';

更改/撤消/重做/等等有两种特殊注入。处理:

  • $ expression已更改的表达式。这可能很简单,比如foo, 如果你使用了深表,那就像f.name一样复杂。
  • $ locals进行更改的范围。如果这很简单, 就像History.watch()一样,你可能不需要它。但如果 你正在深入观察,然后你会想要使用它,因为它没有 代表您最初开始的范围!

$ expression在回滚处理程序中不可用(稍后讨论)。

超凡脱俗的风扇:配料

您可以将一组更改组合在一起并立即撤消它们。注意 目前你必须以某种方式实际观察变化的变量; 您必须先使用watch()或deepWatch(),然后发出批次。

// setup some data
scope.$apply('foo = [1,2,3]');
scope.$apply('bar = "baz"');
scope.$apply(function () {
  scope.data = [
    {id: 1, name: 'foo'},
    {id: 2, name: 'bar'},
    {id: 3, name: 'baz'}
  ];
  scope.otherdata = {
    1: {
      name: 'foo'
    },
    2: {
      name: 'bar'
    },
    3: {
      name: 'baz'
    }
  };
});

// watch some of these things through various means
History.watch('foo', scope, 'foo array changed');
History.watch('bar', scope, 'bar string changed');
History.deepWatch('d.name for d in data', scope, 'name in data changed');
History.deepWatch('od.name for (key, od) in otherdata', scope,
  'name in otherdata changed');

// change some things outright
scope.$apply('pigs = "chickens"');
scope.$apply('foo = [4,5,6]');

接下来我们将启动批处理。您将收到一个特殊的新范围 监视对象,然后可以传递给History.rollback(),它将滚动 返回批量关闭中所做的所有更改。见下文。

传递给History.batch()的函数将接受范围参数,和 这实际上是真实范围的子范围。这里进行了更改 传播到父母的历史。

注意第二个参数,它是可选的,默认为$ rootScope, 就像API中的其他功能一样。

var w = History.batch(function (child) {
  child.$apply('foo[0] = 7');
  child.$apply('foo[1] = 8');
  child.$apply('foo[2] = 9'); // 3 changes to "foo"
  child.$apply('data[0].name = "marvin"'); // one change to the "data" array
  child.$apply('otherdata[1].name = "pookie"'); // one change to the "otherdata" array
  child.$apply('bar = "spam"'); // change to a string
  child.$apply('pigs = "cows"'); // change to something *not* watched
}, scope);

除非您愿意,否则不必使用子变量。它的 完全合法的,如果你已经在一个消化循环中,只是做作业。 如果您已经在摘要中(如果您已经在摘要中),以下内容也是等效的 在控制器中使用此代码,您可能在摘要中;如果你正在使用 它在一个指令中,你可能会或可能不会取决于你所做的事情):

var w = History.batch(function (child) {
  scope.foo[0] = 7;
  scope.foo[1] = 8;
  scope.foo[2] = 9;
  scope.data[0].name = "marvin";
  scope.otherdata[1].name = "pookie";
  scope.bar = "spam";
  scope.pigs = "cows";
}, scope);

让我们确保通知我们可爱的控制台发生回滚:

w.addRollbackHandler('myRollbackHandler', function($locals) {
  // $locals is the same as the "transaction" property of the watch object "w"
  console.log('a rollback happened against scope ' + $locals.$id);
});

现在让我们发布回滚。请注意,您也可以收听 History.batchBegan和History.batchEnded从中广播的事件 $ rootScope,如果你想实现更多的一般功能。

var transaction = w.transaction;
History.rollback(transaction);

让我们通过查看一些断言来看看我们最终得到的结果:

Q.deepEqual(scope.foo, [4, 5, 6], 'foo is again [4,5,6]');
Q.equal(scope.bar, 'baz', 'bar is again baz');
// no change here, because we didn't watch "pigs"
Q.equal(scope.pigs, 'cows', 'pigs is still cows (no change)');
Q.equal(scope.data[0].name, 'foo', 'data[0].name is again "foo"');
Q.equal(scope.otherdata[1].name, 'foo', 'otherdata[1].name is again foo');

// see that you can undo further in some cases
History.undo('foo', scope);
Q.deepEqual(scope.foo, [1, 2, 3], 'foo is again [1,2,3]');

// see you can redo again
History.redo('foo', scope);
Q.deepEqual(scope.foo, [4, 5, 6], 'foo is again [4,5,6]');

// but also that you can't redo past the rollback.
// I suppose this could change, but it would put a lot
// of extra crap in the history.
Q.ok(!History.canRedo('foo', scope), 'assert no more history');

此批处理尚未使用前面提到的“惰性”功能进行测试 (还),但它肯定会帮助你支持许多变量的质量变化 一次,并能够向用户报告这些更改。

内幕

要进行调试,您可以通过询问服务来获取堆栈本身:

console.log(History.history);

历史服务的属性包括:

  • 历史记录(所有范围和表达式的完整历史记录堆栈)
  • 指针(它保存指向我们所在历史记录中的索引的指针)
  • watch(这是Scope对象上的实际$ watch函数)
  • lazyWatches(如果你使用它们,它们是存储的“懒惰”手表)
  • watchObjs(存储创建的所有Watch对象)
  • description(存储传递给watch()的任何描述参数)

作者

克里斯托弗希勒在 Decipher,Inc。

本文使用googletrans自动翻译,仅供参考, 原文来自github.com

en_README.md

angular-history Build Status

A history service for AngularJS. Undo/redo, that sort of thing. Has nothing to
do with the "back" button, unless you want it to.

Current Version

0.8.0

Fair warning: Until this project is at 1.0.0 the API is subject to change
and will likely break.

Installation

bower install angular-history

Requirements

Running Tests

Clone this repo and execute:

npm install

to grab the dependencies. Then execute:

grunt test

to run the tests. This will grab the test deps from bower,
and run them against QUnit in a local server on port 8000.

API Documentation

API Documentation

Demo

For now, some in the API documentation.

Usage

First, include the decipher.history module in your application:

angular.module('myApp', ['decipher.history']);

Next, you will want to inject the History service into your component:

angular.module('myApp').controller('MyCtrl', function($scope, History) {
    // ...
});

Optionally, you can grab the ngLazyBind
module if you want to support lazy binding. This becomes useful if you have
say, an <input type="text"> field, and you don't want every keystroke recorded
in the history. If this module is present, the History service will provide
extra options.

Watching an Expression

You will want to give the History service an expression to watch:

angular.module('myApp').controller('MyCtrl', function($scope, History) {
    $scope.foo = 'bar';
    History.watch('foo', $scope);
});

An optional third parameter is the description, which will be emitted when an
item is archived, undone, redone, or reverted, via a $broadcast(). This
allows you to attach to the event and do something with the info, such as pop up
an alert with an "undo" button in it. This value is interpolated against the
passed $scope object.

If you have the ngLazyBind module, you may provide a fourth parameter to
History.watch():

History.watch('foo', $scope, 'Foo changed to {{foo}}', {timeout: 500});

This tells the history stack to update no more often than every 500ms. This
value defaults to 1s. If you wish to use lazy binding with the default, then
simply pass an empty object {} as the fourth parameter. If you do not have
the ngLazyBind module installed, this object will simply be ignored.

The Watch Object

Whenever you issue one of the following commands, you will receive a Watch
object:

  • History.watch()
  • History.deepWatch()
  • History.batch()

This provides another layer of functionality on top of the events that the
History service will broadcast. You can use these chainable objects to run
callbacks depending on the type of action that was taken:

var w = History.watch('foo')
  .addChangeHandler('myChangeHandler', function() {
    console.log('foo got changed');
  })
  .addUndoHandler('myUndoHandler', function() {
    console.log('undid foo.  this message will self-destruct');
    w.removeUndoHandler('myUndoHandler');
  });

The general recommendation is to listen for the global events if you want
general functionality, and to use these Watch object callbacks for specific
functionality.

Undoing/Redoing

Once something is watch()ed, you can undo or redo changes to it, if that
expression is assignable
. If you pass a function as the expression, you may
not undo/redo, but you will still have access to the history stack.

Anyway, to undo, execute:

History.undo('foo', $scope);

The $scope will be updated with the most recent version of the object. You
can undo() as many times as there are changes in the expression's value since
you watch()ed it--this is an entire history stack.

Furthermore, an event will be emitted. The $rootScope will $broadcast() a
History.undone event with the following information:

  • expression: The expression that was undone
  • oldValue: The value the expression was changed from. This is a copy!
  • newValue: The value the expression was changed to
  • description: The optional description you may have passed
  • scope: The scope passed to undo()

Redoing is pretty much as you would expect:

History.redo('foo', $scope);

This only works if you have previously undone something, of course. You can
undo multiple times, then redo multiple times. The event emitted after redo is
History.redone and the information is the same.

Use History.canUndo(exp, scope) and History.canRedo(exp, scope) if you need
to know those things.

Revert

You can revert to the original value at the time of the watch() instruction by
issuing:

History.revert('foo', $scope);

If you are looking at History.history and know where in the stack you want to
go, pass a third parameter and you will revert to a specific revision in the
stack:

History.revert('foo', $scope, 23);

...which will revert directly to the 23rd revision, no questions asked.

In addition, the History.reverted event will return to you the pointer that
you passed it (which is 0 by default).

Forgetting

If you want to stop watching an expression for changes, issue:

History.forget($scope, 'foo');

The history will be purged and the watch will be removed. Note that the $scope parameter comes first. If you omit the second parameter, all of the history data and watches for the scope will be destroyed. If you omit the first parameter, this will wipe the entire History service.

Fanciness: Deep Watching

Maybe it could use a different name, but often situations arise where you want
to watch an entire array of objects for a change in any of those objects'
properties. It would be incredibly inefficient to watch the entire array/object
for changes, and you wouldn't necessarily know what property got updated.

"Deep watch" like so:

$scope.foos = [
  {id: 1, name: 'winken'},
  {id: 2, name: 'blinken'},
  {id: 3, name: 'nod'}
];
History.deepWatch('f.name for f in foos', $scope,
  'Foo with ID {{f.id}} changed to {{f.name}}');

This works for objects as well:

$scope.foos = {
  '1': {name: 'fe'},
  '2': {name: 'fi'},
  '3': {name: 'fo'},
  '4': {name: 'fum'}
};
History.deepWatch('value.name for (key, value) in foos', $scope,
  'Foo with ID {{key}} changed its name to {{value.name}}');

Now, whenever a name of any one of those things changes, history will be put on
the stack.

There are two ways to handle this.

Listen for an event

The first is to listen for the History.archived event:

History.deepWatch('f.name for f in foos', $scope,
  'Foo with ID {{f.id}} changed to {{f.name}}');

$scope.$on('History.archived', function(evt, data) {
  $scope.undo = function() {
    History.undo(data.expression, data.locals);
  };
});

$scope.foos[0].name = 'fuh';

So you can bind to undo() in your view, and it will undo the change to
foos[0].name.

data, as passed to the event handler, will look similar to the
History.undone event as mentioned above:

  • expression: The expression that got archived, local to locals
  • oldValue: The value the expression was changed from. This is a copy!
  • newValue: The value the expression was changed to.
  • description: The optional description you may have passed
  • locals: A scope containing your expression's value. For example, above we
    will have the object f available in locals, with a name property.

Use a handler

The second way is to use a handler built into a Watch object:

var w = History.deepWatch('f.name for f in foos', $scope,
  'Foo with ID {{f.id}} changed to {{f.name}}');

w.addChangeHandler('myChangeHandler', function($expression, $locals, foo) {
  $scope.undo = function() {
    console.log('undoing foo with name "' + foo.name + '"');
    History.undo($expression, $locals);
  });
}, {foo: 'f'});

$scope.foos[0].name = 'fuh';

There are two special injections into your change/undo/redo/etc. handlers:

  • $expression The expression that changed. This may be simple, like foo,
    or complex like f.name if you have used a deep watch.
  • $locals The scope against which the change was made. If this is simple,
    like in the case of a History.watch(), you probably don't need it. But if
    you are doing a deep watch, then you will want to use this, because it does not
    represent the scope you originally started with!

$expression is not available in rollback handlers (discussed later).

Otherworldly Fanciness: Batching

You can group a bunch of changes together and undo them all at once. Note that
currently you must actually be watching the changed variables in some manner;
you must use watch() or deepWatch() first, then issue the batch.

// setup some data
scope.$apply('foo = [1,2,3]');
scope.$apply('bar = "baz"');
scope.$apply(function () {
  scope.data = [
    {id: 1, name: 'foo'},
    {id: 2, name: 'bar'},
    {id: 3, name: 'baz'}
  ];
  scope.otherdata = {
    1: {
      name: 'foo'
    },
    2: {
      name: 'bar'
    },
    3: {
      name: 'baz'
    }
  };
});

// watch some of these things through various means
History.watch('foo', scope, 'foo array changed');
History.watch('bar', scope, 'bar string changed');
History.deepWatch('d.name for d in data', scope, 'name in data changed');
History.deepWatch('od.name for (key, od) in otherdata', scope,
  'name in otherdata changed');

// change some things outright
scope.$apply('pigs = "chickens"');
scope.$apply('foo = [4,5,6]');

Next we'll initiate a batch. You will receive a special new scope within your
Watch object that you can then pass to History.rollback(), which will roll
back all changes made within the batch closure. See below.

The function you pass to History.batch() will accept a scope parameter, and
that is actually a child scope of the real scope. Changes are made here and
propagated to the parent's history.

Note the second parameter, which is optional and defaults to $rootScope,
just like the other functions in the API.

var w = History.batch(function (child) {
  child.$apply('foo[0] = 7');
  child.$apply('foo[1] = 8');
  child.$apply('foo[2] = 9'); // 3 changes to "foo"
  child.$apply('data[0].name = "marvin"'); // one change to the "data" array
  child.$apply('otherdata[1].name = "pookie"'); // one change to the "otherdata" array
  child.$apply('bar = "spam"'); // change to a string
  child.$apply('pigs = "cows"'); // change to something *not* watched
}, scope);

You do not have to use the child variable unless you want to. It's
perfectly legit, if you are in a digest loop already, to just make assignments.
The following is equivalent, again, if you are already in a digest (if you are
using this code in a controller, you are likely in a digest; if you're using
it in a directive, you may or may not be depending on what you're up to):

var w = History.batch(function (child) {
  scope.foo[0] = 7;
  scope.foo[1] = 8;
  scope.foo[2] = 9;
  scope.data[0].name = "marvin";
  scope.otherdata[1].name = "pookie";
  scope.bar = "spam";
  scope.pigs = "cows";
}, scope);

Let's make sure to notify our lovely console that a rollback happened:

w.addRollbackHandler('myRollbackHandler', function($locals) {
  // $locals is the same as the "transaction" property of the watch object "w"
  console.log('a rollback happened against scope ' + $locals.$id);
});

Now let's issue a rollback. Note that you can also listen for the
History.batchBegan and History.batchEnded events that are broadcast from the
$rootScope, if you want to implement more general functionality.

var transaction = w.transaction;
History.rollback(transaction);

Let's see what we ended up with by viewing some assertions:

Q.deepEqual(scope.foo, [4, 5, 6], 'foo is again [4,5,6]');
Q.equal(scope.bar, 'baz', 'bar is again baz');
// no change here, because we didn't watch "pigs"
Q.equal(scope.pigs, 'cows', 'pigs is still cows (no change)');
Q.equal(scope.data[0].name, 'foo', 'data[0].name is again "foo"');
Q.equal(scope.otherdata[1].name, 'foo', 'otherdata[1].name is again foo');

// see that you can undo further in some cases
History.undo('foo', scope);
Q.deepEqual(scope.foo, [1, 2, 3], 'foo is again [1,2,3]');

// see you can redo again
History.redo('foo', scope);
Q.deepEqual(scope.foo, [4, 5, 6], 'foo is again [4,5,6]');

// but also that you can't redo past the rollback.
// I suppose this could change, but it would put a lot
// of extra crap in the history.
Q.ok(!History.canRedo('foo', scope), 'assert no more history');

This batching hasn't been tested with the "lazy" functionality mentioned earlier
(yet), but it will certainly help you support mass changes to many variables at
once, and be able to report those changes to the user.

Internals

To debug, you can grab the stack itself by asking the service for it:

console.log(History.history);

Properties of the History service include:

  • history (the complete history stack for all of the scopes and expressions)
  • pointers (which keeps a pointer to the index in the history we are at)
  • watches (which are the actual $watch functions on the Scope objects)
  • lazyWatches (which are the stored "lazy" watches if you are using them)
  • watchObjs (which stores all Watch objects created)
  • descriptions (which stores any description parameters passed to watch())

Author

Christopher Hiller at
Decipher, Inc.