immer#applyPatches TypeScript Examples

The following examples show how to use immer#applyPatches. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.ts    From core with MIT License 6 votes vote down vote up
function topReducer(state: any, action: any) {
  switch (action.type) {
    case 'PATCH': {
      return produce(state, (draftState: any) => {
        const patch = {
          ...action.payload.patch,
          path: jsonPatchPathToImmerPath(action.payload.patch.path),
        };
        draftState[action.payload.subtree].state = applyPatches(
          draftState[action.payload.subtree].state,
          [patch]
        );
        draftState[action.payload.subtree].patches.push({
          patch: action.payload.patch,
          inversePatch: action.payload.inversePatch,
        });
      });
    }

    case 'CREATE_SUBTREE': {
      return produce(state, (draftState: any) => {
        draftState[action.payload.subtree] = {
          state: action.payload.initialState,
          patches: [],
        };
      });
    }

    default:
      return state;
  }
}
Example #2
Source File: useStateTracking.ts    From baleen3 with Apache License 2.0 5 votes vote down vote up
export function useStateTracking<T>(initialState: T): {
  current: T
  modify: (mutator: Mutator<T>) => void
  initialize: (initializer: Initializer<T>) => void
  undo: () => void
  redo: () => void
  canUndo: boolean
  canRedo: boolean
} {
  const [current, setCurrent] = useState<T>(initialState)
  const undoStack = useRef<
    Array<{ patches: Patch[]; inversePatches: Patch[] }>
  >([])
  const undoStackPointer = useRef(-1)

  const undo = useCallback((): void => {
    if (undoStackPointer.current < 0) {
      return
    }

    setCurrent(
      (state) =>
        applyPatches(
          state,
          undoStack.current[undoStackPointer.current].inversePatches
        ) as T
    )
    undoStackPointer.current--
  }, [])

  const redo = useCallback((): void => {
    if (undoStackPointer.current === undoStack.current.length - 1) {
      return
    }
    undoStackPointer.current++

    setCurrent(
      (state) =>
        applyPatches(
          state,
          undoStack.current[undoStackPointer.current].patches
        ) as T
    )
  }, [])

  const modify = useCallback((mutator: Mutator<T>): void => {
    setCurrent((state) =>
      produce(state, mutator, (patches, inversePatches) => {
        // ignore empty change
        if (patches.length > 0) {
          const pointer = ++undoStackPointer.current
          undoStack.current.length = pointer
          undoStack.current[Number(pointer)] = { patches, inversePatches }
        }
      })
    )
  }, [])

  const initialize = useCallback((initializer: Initializer<T>): void => {
    setCurrent(initializer)
    undoStackPointer.current = -1
    undoStack.current.length = 0
  }, [])

  return {
    current,
    initialize,
    modify,
    undo,
    redo,
    canUndo: undoStackPointer.current > -1,
    canRedo: undoStackPointer.current < undoStack.current.length - 1,
  }
}
Example #3
Source File: history.ts    From textbus with GNU General Public License v3.0 4 votes vote down vote up
private apply(historyItem: HistoryItem, back: boolean) {
    let operations = historyItem.operations
    if (back) {
      operations = [...operations].reverse()
    }
    operations.forEach(op => {
      const path = [...op.path]
      const isFindSlot = path.length % 2 === 1
      const actions = back ? op.unApply : op.apply

      if (isFindSlot) {
        const slot = this.selection.findSlotByPaths(path)!
        actions.forEach(action => {
          if (action.type === 'retain') {
            if (action.formats) {
              const formats: Formats = []
              Object.keys(action.formats).forEach(i => {
                const formatter = this.registry.getFormatter(i)
                if (formatter) {
                  formats.push([formatter, action.formats![i]])
                }
              })
              slot.retain(action.offset, formats)
            } else {
              slot.retain(action.offset)
            }
            return
          }
          if (action.type === 'delete') {
            slot.delete(action.count)
            return
          }
          if (action.type === 'apply') {
            slot.updateState(draft => {
              applyPatches(draft, action.patches)
            })
            return
          }
          if (action.type === 'insert') {
            if (typeof action.content === 'string') {
              if (action.formats) {
                const formats: Formats = []
                Object.keys(action.formats).forEach(i => {
                  const formatter = this.registry.getFormatter(i)
                  if (formatter) {
                    formats.push([formatter, action.formats![i]])
                  }
                })
                slot.insert(action.content, formats)
              } else {
                slot.insert(action.content)
              }
            } else {
              const component = this.translator.createComponent(action.content as ComponentLiteral)!
              slot.insert(component)
            }
          }
        })
      } else {
        const component = this.selection.findComponentByPaths(path)!
        actions.forEach(action => {
          if (action.type === 'retain') {
            component.slots.retain(action.offset)
            return
          }
          if (action.type === 'delete') {
            component.slots.delete(action.count)
            return
          }
          if (action.type === 'insertSlot') {
            const slot = this.translator.createSlot(action.slot)
            component.slots.insert(slot)
          }
          if (action.type === 'apply') {
            component.updateState(draft => {
              return applyPatches(draft, action.patches)
            })
            return
          }
        })
      }
    })
  }
Example #4
Source File: index.test.ts    From reactant with MIT License 4 votes vote down vote up
test('base model with `useValue` and `enablePatches`', () => {
  const todoList: string[] = [];

  const todoModel = model({
    name: 'todo',
    state: {
      todoList,
    },
    actions: {
      add: (todo: string) => (state) => {
        state.todoList.push(todo);
      },
    },
  });

  @injectable()
  class Foo {
    constructor(@inject('todo') public todo: typeof todoModel) {}

    add(todo: string) {
      this.todo.add(todo);
    }

    get todoList() {
      return this.todo.todoList;
    }
  }

  const actionFn = jest.fn();

  class Logger extends PluginModule {
    middleware: ReactantMiddleware = (store) => (next) => (_action) => {
      actionFn(_action);
      return next(_action);
    };
  }

  const ServiceIdentifiers = new Map();
  const modules = [Foo, { provide: 'todo', useValue: todoModel }, Logger];
  const container = createContainer({
    ServiceIdentifiers,
    modules,
    options: {
      defaultScope: 'Singleton',
    },
  });
  const foo = container.get(Foo);
  const store = createStore({
    modules,
    container,
    ServiceIdentifiers,
    loadedModules: new Set(),
    load: (...args: any[]) => {
      //
    },
    pluginHooks: {
      middleware: [],
      beforeCombineRootReducers: [],
      afterCombineRootReducers: [],
      enhancer: [],
      preloadedStateHandler: [],
      afterCreateStore: [],
      provider: [],
    },
    devOptions: {
      enablePatches: true,
    },
  });
  const originalTodoState = foo.todo[stateKey]!;
  expect(Object.values(store.getState())).toEqual([{ todoList: [] }]);
  expect(actionFn.mock.calls.length).toBe(0);
  foo.add('test');
  expect(Object.values(store.getState())).toEqual([{ todoList: ['test'] }]);
  expect(actionFn.mock.calls.length).toBe(1);
  expect(actionFn.mock.calls[0][0]._patches).toEqual([
    {
      op: 'add',
      path: ['todoList', 0],
      value: 'test',
    },
  ]);
  expect(actionFn.mock.calls[0][0]._inversePatches).toEqual([
    {
      op: 'replace',
      path: ['todoList', 'length'],
      value: 0,
    },
  ]);
  expect(
    applyPatches(originalTodoState, actionFn.mock.calls[0][0]._patches)
  ).toEqual(foo.todo[stateKey]);
  expect(
    applyPatches(originalTodoState, actionFn.mock.calls[0][0]._patches) ===
      foo.todo[stateKey]
  ).toBe(false);
  expect(
    applyPatches(foo.todo[stateKey]!, actionFn.mock.calls[0][0]._inversePatches)
  ).toEqual(originalTodoState);
});
Example #5
Source File: action.test.ts    From reactant with MIT License 4 votes vote down vote up
describe('@action', () => {
  test('base', () => {
    @injectable()
    class Counter {
      @state
      count = 0;

      @action
      increase() {
        this.count += 1;
      }
    }
    const ServiceIdentifiers = new Map();
    const modules = [Counter];
    const container = createContainer({
      ServiceIdentifiers,
      modules,
      options: {
        defaultScope: 'Singleton',
      },
    });
    const counter = container.get(Counter);
    const store = createStore({
      modules,
      container,
      ServiceIdentifiers,
      loadedModules: new Set(),
      load: (...args: any[]) => {
        //
      },
      pluginHooks: {
        middleware: [],
        beforeCombineRootReducers: [],
        afterCombineRootReducers: [],
        enhancer: [],
        preloadedStateHandler: [],
        afterCreateStore: [],
        provider: [],
      },
    });
    counter.increase();
    expect(counter.count).toBe(1);
    expect((counter as any)[enablePatchesKey]).toBe(false);
    expect(Object.values(store.getState())).toEqual([{ count: 1 }]);
  });

  test('enable `autoFreeze` in devOptions', () => {
    @injectable({
      name: 'counter',
    })
    class Counter {
      @state
      count = 0;

      @state
      sum = { count: 0 };

      @action
      increase() {
        this.sum.count += 1;
      }

      increase1() {
        this.sum.count += 1;
      }

      increase2() {
        this.count += 1;
      }
    }
    const ServiceIdentifiers = new Map();
    const modules = [Counter];
    const container = createContainer({
      ServiceIdentifiers,
      modules,
      options: {
        defaultScope: 'Singleton',
      },
    });
    const counter = container.get(Counter);
    const store = createStore({
      modules,
      container,
      ServiceIdentifiers,
      loadedModules: new Set(),
      load: (...args: any[]) => {
        //
      },
      pluginHooks: {
        middleware: [],
        beforeCombineRootReducers: [],
        afterCombineRootReducers: [],
        enhancer: [],
        preloadedStateHandler: [],
        afterCreateStore: [],
        provider: [],
      },
      devOptions: { autoFreeze: true },
    });
    expect(() => {
      store.getState().counter.sum.count = 1;
    }).toThrowError(/Cannot assign to read only property/);
    counter.increase();
    for (const fn of [
      () => {
        store.getState().counter.sum.count = 1;
      },
      () => counter.increase1(),
      () => counter.increase2(),
    ]) {
      expect(fn).toThrowError(/Cannot assign to read only property/);
    }
  });

  test('inherited module with stagedState about more effects', () => {
    @injectable({
      name: 'foo0',
    })
    class Foo0 {
      @state
      count0 = 1;

      @state
      count1 = 1;

      @action
      increase() {
        this.count0 += 1;
      }

      @action
      decrease() {
        this.count0 -= 1;
      }

      @action
      decrease1() {
        this.count0 -= 1;
      }
    }

    @injectable({
      name: 'foo',
    })
    class Foo extends Foo0 {
      count = 1;

      add(count: number) {
        this.count += count;
      }

      @action
      increase() {
        // inheritance
        super.increase();
        // change state
        this.count0 += 1;
        // call other action function
        this.increase1();
        // call unwrapped `@action` function
        this.add(this.count);
      }

      @action
      increase1() {
        this.count1 += 1;
      }

      @action
      decrease() {
        super.decrease();
        this.count0 -= 1;
      }

      decrease1() {
        super.decrease1();
      }
    }

    @injectable()
    class FooBar {
      constructor(public foo: Foo, public foo0: Foo0) {}
    }

    const ServiceIdentifiers = new Map();
    const modules = [FooBar];
    const container = createContainer({
      ServiceIdentifiers,
      modules,
      options: {
        defaultScope: 'Singleton',
      },
    });
    const fooBar = container.get(FooBar);
    const store = createStore({
      modules,
      container,
      ServiceIdentifiers,
      loadedModules: new Set(),
      load: (...args: any[]) => {
        //
      },
      pluginHooks: {
        middleware: [],
        beforeCombineRootReducers: [],
        afterCombineRootReducers: [],
        enhancer: [],
        preloadedStateHandler: [],
        afterCreateStore: [],
        provider: [],
      },
    });
    const subscribe = jest.fn();
    store.subscribe(subscribe);
    fooBar.foo.increase();
    expect(fooBar.foo.count0).toBe(3);
    expect(fooBar.foo.count1).toBe(2);
    expect(fooBar.foo.count).toBe(2);
    fooBar.foo0.increase();
    expect(fooBar.foo.count0).toBe(3);
    expect(fooBar.foo.count1).toBe(2);
    expect(fooBar.foo.count).toBe(2);
    // merge the multi-actions changed states as one redux dispatch.
    expect(subscribe.mock.calls.length).toBe(2);
    // inheritance
    fooBar.foo.decrease();
    expect(fooBar.foo.count0).toBe(1);
    fooBar.foo.decrease1();
    expect(fooBar.foo.count0).toBe(0);
  });

  test('across module changing state', () => {
    @injectable()
    class Foo {
      @state
      textList: string[] = [];

      @action
      addText(text: string) {
        this.textList.push(text);
      }
    }

    @injectable()
    class Counter {
      constructor(public foo: Foo) {}

      @state
      count = 0;

      @action
      increase() {
        this.foo.addText(`test${this.count}`);
        this.count += 1;
        this.foo.addText(`test${this.count}`);
        this.count += 1;
        this.foo.addText(`test${this.count}`);
      }
    }

    const ServiceIdentifiers = new Map();
    const modules = [Counter];
    const container = createContainer({
      ServiceIdentifiers,
      modules,
      options: {
        defaultScope: 'Singleton',
      },
    });
    const counter = container.get(Counter);
    const store = createStore({
      modules,
      container,
      ServiceIdentifiers,
      loadedModules: new Set(),
      load: (...args: any[]) => {
        //
      },
      pluginHooks: {
        middleware: [],
        beforeCombineRootReducers: [],
        afterCombineRootReducers: [],
        enhancer: [],
        preloadedStateHandler: [],
        afterCreateStore: [],
        provider: [],
      },
    });
    const subscribeFn = jest.fn();
    store.subscribe(subscribeFn);
    counter.increase();
    expect(subscribeFn.mock.calls.length).toBe(1);
    expect(counter.count).toEqual(2);
    expect(counter.foo.textList).toEqual(['test0', 'test1', 'test2']);
  });

  test('across module changing state with error', () => {
    @injectable({
      name: 'foo',
    })
    class Foo {
      @state
      list: number[] = [];

      @action
      addItem(num: number) {
        if (num === 1) {
          // eslint-disable-next-line no-throw-literal
          throw 'something error';
        } else {
          this.list.push(num);
        }
      }
    }

    @injectable({
      name: 'counter',
    })
    class Counter {
      constructor(public foo: Foo) {}

      @state
      count = 0;

      @action
      increase() {
        this.foo.addItem(this.count);
        this.count += 1;
      }

      @action
      increase1() {
        this.foo.addItem(this.count + 1);
        this.count += 1;
      }
    }

    const ServiceIdentifiers = new Map();
    const modules = [Counter];
    const container = createContainer({
      ServiceIdentifiers,
      modules,
      options: {
        defaultScope: 'Singleton',
      },
    });
    const counter = container.get(Counter);
    const store = createStore({
      modules,
      container,
      ServiceIdentifiers,
      loadedModules: new Set(),
      load: (...args: any[]) => {
        //
      },
      pluginHooks: {
        middleware: [],
        beforeCombineRootReducers: [],
        afterCombineRootReducers: [],
        enhancer: [],
        preloadedStateHandler: [],
        afterCreateStore: [],
        provider: [],
      },
    });
    const subscribeFn = jest.fn();
    store.subscribe(subscribeFn);
    counter.increase();
    expect(subscribeFn.mock.calls.length).toBe(1);

    expect(() => {
      counter.increase();
    }).toThrowError('something error');
    expect(getStagedState()).toBeUndefined();
    expect(subscribeFn.mock.calls.length).toBe(1);

    counter.increase1();
    expect(subscribeFn.mock.calls.length).toBe(2);
    expect(counter.count).toBe(2);
    expect(counter.foo.list).toEqual([0, 2]);
    expect(store.getState()).toEqual({
      counter: { count: 2 },
      foo: { list: [0, 2] },
    });
  });

  test('base with `enablePatches`', () => {
    interface Todo {
      text: string;
    }
    @injectable({
      name: 'todo',
    })
    class TodoList {
      @state
      list: Todo[] = [
        {
          text: 'foo',
        },
      ];

      @action
      add(text: string) {
        this.list.slice(-1)[0].text = text;
        this.list.push({ text });
      }
    }

    const actionFn = jest.fn();

    const middleware: Middleware = (store) => (next) => (_action) => {
      actionFn(_action);
      return next(_action);
    };

    const ServiceIdentifiers = new Map();
    const modules = [TodoList, applyMiddleware(middleware)];
    const container = createContainer({
      ServiceIdentifiers,
      modules,
      options: {
        defaultScope: 'Singleton',
      },
    });
    const todoList = container.get(TodoList);
    const store = createStore({
      modules,
      container,
      ServiceIdentifiers,
      loadedModules: new Set(),
      load: (...args: any[]) => {
        //
      },
      pluginHooks: {
        middleware: [],
        beforeCombineRootReducers: [],
        afterCombineRootReducers: [],
        enhancer: [],
        preloadedStateHandler: [],
        afterCreateStore: [],
        provider: [],
      },
      devOptions: {
        enablePatches: true,
      },
    });
    const originalTodoState = store.getState();
    expect(Object.values(store.getState())).toEqual([
      { list: [{ text: 'foo' }] },
    ]);
    expect(actionFn.mock.calls.length).toBe(0);
    todoList.add('test');
    expect(Object.values(store.getState())).toEqual([
      { list: [{ text: 'test' }, { text: 'test' }] },
    ]);
    expect(actionFn.mock.calls.length).toBe(1);
    expect(actionFn.mock.calls[0][0]._patches).toEqual([
      {
        op: 'replace',
        path: ['todo', 'list', 0, 'text'],
        value: 'test',
      },
      {
        op: 'add',
        path: ['todo', 'list', 1],
        value: {
          text: 'test',
        },
      },
    ]);
    expect(actionFn.mock.calls[0][0]._inversePatches).toEqual([
      {
        op: 'replace',
        path: ['todo', 'list', 0, 'text'],
        value: 'foo',
      },
      {
        op: 'replace',
        path: ['todo', 'list', 'length'],
        value: 1,
      },
    ]);
    expect(
      applyPatches(originalTodoState, actionFn.mock.calls[0][0]._patches)
    ).toEqual(store.getState());
    expect(
      applyPatches(originalTodoState, actionFn.mock.calls[0][0]._patches) ===
        store.getState()
    ).toBe(false);
    expect(
      applyPatches(store.getState(), actionFn.mock.calls[0][0]._inversePatches)
    ).toEqual(originalTodoState);
  });
});