How to save Mobx state in sessionStorage - reactjs

Trying to essentially accomplish this https://github.com/elgerlambert/redux-localstorage which is for Redux but do it for Mobx. And preferably would like to use sessionStorage. Is there an easy way to accomplish this with minimal boilerplate?

The easiest way to approach this would be to have a mobx "autorun" triggered whenever any observable property changes. To do that, you could follow my answer to this question.
I'll put some sample code here that should help you get started:
function autoSave(store, save) {
let firstRun = true;
mobx.autorun(() => {
// This code will run every time any observable property
// on the store is updated.
const json = JSON.stringify(mobx.toJS(store));
if (!firstRun) {
save(json);
}
firstRun = false;
});
}
class MyStore {
#mobx.observable prop1 = 999;
#mobx.observable prop2 = [100, 200];
constructor() {
this.load();
autoSave(this, this.save.bind(this));
}
load() {
if (/* there is data in sessionStorage */) {
const data = /* somehow get the data from sessionStorage or anywhere else */;
mobx.extendObservable(this, data);
}
}
save(json) {
// Now you can do whatever you want with `json`.
// e.g. save it to session storage.
alert(json);
}
}

Turns out you can do this in just a few lines of code:
const store = observable({
players: [
"Player 1",
"Player 2",
],
// ...
})
reaction(() => JSON.stringify(store), json => {
localStorage.setItem('store',json);
}, {
delay: 500,
});
let json = localStorage.getItem('store');
if(json) {
Object.assign(store, JSON.parse(json));
}
Boom. No state lost when I refresh the page. Saves every 500ms if there was a change.

Posting the example from here: https://mobx.js.org/best/store.html
This shows a cleaner method of detecting value changes, though not necessarily local storage.
import {observable, autorun} from 'mobx';
import uuid from 'node-uuid';
export class TodoStore {
authorStore;
transportLayer;
#observable todos = [];
#observable isLoading = true;
constructor(transportLayer, authorStore) {
this.authorStore = authorStore; // Store that can resolve authors for us
this.transportLayer = transportLayer; // Thing that can make server requests for us
this.transportLayer.onReceiveTodoUpdate(updatedTodo => this.updateTodoFromServer(updatedTodo));
this.loadTodos();
}
/**
* Fetches all todo's from the server
*/
loadTodos() {
this.isLoading = true;
this.transportLayer.fetchTodos().then(fetchedTodos => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json));
this.isLoading = false;
});
}
/**
* Update a todo with information from the server. Guarantees a todo
* only exists once. Might either construct a new todo, update an existing one,
* or remove an todo if it has been deleted on the server.
*/
updateTodoFromServer(json) {
var todo = this.todos.find(todo => todo.id === json.id);
if (!todo) {
todo = new Todo(this, json.id);
this.todos.push(todo);
}
if (json.isDeleted) {
this.removeTodo(todo);
} else {
todo.updateFromJson(json);
}
}
/**
* Creates a fresh todo on the client and server
*/
createTodo() {
var todo = new Todo(this);
this.todos.push(todo);
return todo;
}
/**
* A todo was somehow deleted, clean it from the client memory
*/
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1);
todo.dispose();
}
}
export class Todo {
/**
* unique id of this todo, immutable.
*/
id = null;
#observable completed = false;
#observable task = "";
/**
* reference to an Author object (from the authorStore)
*/
#observable author = null;
store = null;
/**
* Indicates whether changes in this object
* should be submitted to the server
*/
autoSave = true;
/**
* Disposer for the side effect that automatically
* stores this Todo, see #dispose.
*/
saveHandler = null;
constructor(store, id=uuid.v4()) {
this.store = store;
this.id = id;
this.saveHandler = reaction(
// observe everything that is used in the JSON:
() => this.asJson,
// if autoSave is on, send json to server
(json) => {
if (this.autoSave) {
this.store.transportLayer.saveTodo(json);
}
}
);
}
/**
* Remove this todo from the client and server
*/
delete() {
this.store.transportLayer.deleteTodo(this.id);
this.store.removeTodo(this);
}
#computed get asJson() {
return {
id: this.id,
completed: this.completed,
task: this.task,
authorId: this.author ? this.author.id : null
};
}
/**
* Update this todo with information from the server
*/
updateFromJson(json) {
// make sure our changes aren't send back to the server
this.autoSave = false;
this.completed = json.completed;
this.task = json.task;
this.author = this.store.authorStore.resolveAuthor(json.authorId);
this.autoSave = true;
}
dispose() {
// clean up the observer
this.saveHandler();
}
}

Here, you can use my code, although it only supports localStorage you should be able to modify it quite easily.
https://github.com/nightwolfz/mobx-storage

Related

Validation on export data for one2many field column in Odoo 13

I want to show validation error when the user export records for state='draft'(in one2many field). I have done code for it and it's working fine. but when I put this code for one2many table then I unable to get a validation message.
My code is below:
class DailyTransaction(models.Model):
_name = 'daily.transaction'
_rec_name = 'batch_id'
date = fields.Date()
batch_id = fields.Char()
daily_transaction = fields.One2many('transaction.log', 'daily_trans_log', string='Daily Transaction')
class Transaction_log(models.Model):
_name = 'transaction.log'
_rec_name = 'daily_trans_log'
daily_trans_log = fields.Many2one('daily.transaction')
log_status = fields.Selection([('Draft', 'Draft'), ('Approved', 'Approved'), ('Confirmed', 'Confirmed')],
default='Draft', string='Log Status')
odoo.define("transaction_log.export_log", function(require) {
"use strict";
var listController = require("web.ListController");
var dialog = require("web.Dialog");
listController.include({
/**
* Opens the Export Dialog
*
* #private
*/
_onExportData: function () {
var self = this;
var do_export = true;
// Avoid calling `read` when `state` field is not available
if (self.initialState.fields.hasOwnProperty('log_status')) {
self._rpc({
model: self.modelName,
method: 'read',
args: [self.getSelectedIds(), ['log_status']],
}).then(function (result) {
// Check if we have at least one draft record
for(var index in result) {
var item = result[index];
if (item.log_status === 'Draft') {
do_export = false;
break;
}
}
if (do_export) {
self._getExportDialogWidget().open();
} else {
dialog.alert(self, "You can't export draft stage data!", {});
}
});
} else {
this._getExportDialogWidget().open();
}
},
});
});
when I export record from 'transaction.log' for 'Draft' log_status then it's work and shows validation message. But I also want to show this validation when export from 'daily.transaction'
Thanks in advance.
You need to add a second condition and read records from the related model to check if there is some record in Draft state.
else if (self.initialState.fields.hasOwnProperty('daily_transaction')){
self._rpc({
model: 'transaction.log',
method: 'search_read',
args: [[['daily_trans_log', 'in', self.getSelectedIds()]], ['log_status']],
}).then(function (result) {
// Check if we have at least one draft record
for(var index in result) {
var item = result[index];
if (item.log_status === 'Draft') {
do_export = false;
break;
}
}
if (do_export) {
self._getExportDialogWidget().open();
} else {
dialog.alert(self, "You can't export draft stage data!", {});
}
});
}
The code after then is the same, I just made a quick example.

Component not re-rendering when nested observable changes

Adding a node to a list and yet a component is not re-rendering. Mobx Chrome Extension dev tools says it's a dependency but for some reason still no reaction!
A button renders 'Add' or 'Remove' based on whether a node is in a list. It doesn't re-render unless I move to another component and then open this component again.
Buttons:
#inject("appStore") #observer
class EntityTab extends Component {
...
render() {
return (
...
{/* BUTTONS */}
{ this.props.appStore.repo.canvas.graph.structure.dictionary[id] !== undefined ?
<div onClick={() => this.props.appStore.repo.canvas.graph.function(id)}>
<p>Remove</p>
</div>
:
<div onClick={() => this.props.appStore.currRepo.canvas.otherfunction(id)}>
<p>Add</p>
</div>
}
...
)
}
}
The Add button renders, I click on the button which triggers
this.props.appStore.currRepo.canvas.otherfunction(id)
and then we go to this function
#observable graph = new Graph();
...
#action
otherfunction = (idList) => {
// Do nothing if no ids are given
if (idList === null || (Array.isArray(idList) && idList.length === 0)) return;
// If idList is a single value, wrap it with a list
if (!Array.isArray(idList)) { idList = [idList] }
let nodesToAdd = [];
let linksToAdd = [];
// Add all new links/nodes to graph
Promise.all(idList.map((id) => { return this.getNode(id, 1) }))
.then((responses) => {
for (let i = 0; i < responses.length; i++) {
let res = responses[i];
if (res.success) {
nodesToAdd.push(...res.data.nodes);
linksToAdd.push(...res.data.links);
}
}
this.graph.addData(nodesToAdd, linksToAdd, idList, this.sidebarVisible);
});
};
The getNode function creates new Node objects from the data. For reference, those objects are instantiated as such
export default class Node {
id = '';
name = '';
type = '';
constructor(r) {
for (let property in r) {
// Set Attributes
this[property] = r[property];
}
}
}
anyway, the addToGraphFromIds triggers
this.graph.addData(nodesToAdd, linksToAdd);
and then we go to that function
#action
addData = (nodes, links) => {
this.structure.addNodes(nodes);
this.structure.addLinks(links);
};
which triggers
this.structure.addNodes(nodes);
which leads to this function
#observable map = new Map();
#observable map2 = new Map();
#observable dictionary = {};
#observable dictionary2 = {};
#observable allNodes = [];
#observable allLinks = [];
...
#action
addNodes = (nodes=[]) => {
if (!nodes || nodes.length === 0) return;
nodes = utils.toArray(nodes);
// Only consider each new node if it's not in the graph or a duplicate within the input list
nodes = _.uniqBy(nodes, (obj) => { return obj.id; });
const nodesToConsider = _.differenceBy(nodes, this.allNodes, (obj) => { return obj.id; });
// Add nodes to map
let currNode;
for (let i = 0; i < nodesToConsider.length; i++) {
currNode = nodesToConsider[i];
this.map.set(currNode.id, new Map());
this.map2.set(currNode.id, new Map());
this.dictionary[currNode.id] = currNode;
}
// Update internal list of nodes
this.allNodes = this.allNodes.concat(nodesToConsider);
};
As we can see in the first codebox,
this.props.appStore.repo.canvas.graph.structure.dictionary[id] !== undefined
Should cause the button to change values as we have added the current node. The nodes appear in the dictionary when I log or use mobx chrome extension dev tools, but I have to switch tabs and then the button will re-render. I've tried using other lines like
this.props.appStore.repo.canvas.graph.structure.allNodes.includes(node)
but that doesn't work either. Am absolutely stuck and need help. I have a feeling it has to do with nested observables, and maybe tagging #observable isn't good enough, but not quite sure. repo and canvas are marked as observables and instantiate a new Repo() object and new Canvas() object, much like new Node() is created in getNodes.
Mobx (v4) does not track addition or removal of entries in an observable object unless observable.map is used. Or upgrading to mobx v5 should solve the issue.
For your specific issue you can try:
#observable nodeIdToNodeData = {};
//...
this.nodeIdToNodeData = {...this.nodeIdToNodeData, [currNode.id]: currNode};
Or try to upgrade to mobx v5.
More info here
Looks like Edward solved it, similar to Redux it looks like you have to create a new object with the dictionary rather than modify it. I'm guessing it's because the key currNode.id is already defined (as the value undefined), and we're just modifying it to be currNode. That's my guess, regardless it works now.

relay prisma graphql update store

I am trying to get prisma and relay working. Here's my repo:
https://github.com/jamesmbowler/prisma-relay-todo
It's a simple todo list. I am able to add the todos, but the ui does not update. When I refresh, the todo is there.
All of the examples of updating the store, that I can find, use a "parent" to the object that is being updated / created.
See https://facebook.github.io/relay/docs/en/mutations.html#using-updater-and-optimisticupdater
Also, the "updater configs" also requires a "parentID". https://facebook.github.io/relay/docs/en/mutations.html#updater-configs
From relay-runtime's RelayConnectionHandler.js comment here:
https://github.com/facebook/relay/blob/master/packages/relay-runtime/handlers/connection/RelayConnectionHandler.js#L232
* store => {
* const user = store.get('<id>');
* const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
* const edge = store.create('<edge-id>', 'FriendsEdge');
* RelayConnectionHandler.insertEdgeAfter(friends, edge);
* }
Is it possible to update the store without having a "parent"? I just have todos, with no parent.
Again, creating the record works, and gives this response:
{ "data" : {
"createTodo" : {
"id" : "cjpdbivhc00050903ud6bkl3x",
"name" : "testing",
"complete" : false
} } }
Here's my updater function
updater: store => {
const payload = store.getRootField('createTodo');
const conn = ConnectionHandler.getConnection(store.get(payload.getDataID()), 'Todos_todoesConnection');
ConnectionHandler.insertEdgeAfter(conn, payload, cursor);
},
I have done a console.log(conn), and it is undefined.
Please help.
----Edit----
Thanks to Denis, I think one problem is solved - that of the ConnectionHandler.
But, I still can't get the ui to update. Here's what I've tried in the updater function:
const payload = store.getRootField('createTodo');
const clientRoot = store.get('client:root');
const conn = ConnectionHandler.getConnection(clientRoot, 'Todos_todoesConnection');
ConnectionHandler.createEdge(store, conn, payload, 'TodoEdge');
I've also tried this:
const payload = store.getRootField('createTodo');
const clientRoot = store.get('client:root');
const conn = ConnectionHandler.getConnection(clientRoot, 'Todos_todoesConnection');
ConnectionHandler.insertEdgeAfter(conn, payload);
My data shape is different from their example, as I don't have the 'todoEdge', and 'node' inside my returned data (see above).
todoEdge {
cursor
node {
complete
id
text
}
}
How do I getLinkedRecord, like this?
const newEdge = payload.getLinkedRecord('todoEdge');
If query is the parent, the parentID will be client:root.
Take a look at this: https://github.com/facebook/relay/blob/1d72862fa620a9db69d6219d5aa562054d9b93c7/packages/react-relay/classic/store/RelayStoreConstants.js#L18
Also at this issue: https://github.com/facebook/relay/issues/2157#issuecomment-385009482
create a const ROOT_ID = 'client:root'; and pass ROOT_ID as your parentID. Also, check the name of your connection on the updater, it has to be exactly equal to the name where you declared the query.
UPDATE:
Actually, you can import ROOT_ID of relay-runtime
import { ROOT_ID } from 'relay-runtime';
UPDATE 2:
Your edit was not very clear for me, but I will provide you an example of it should work ok? After your mutation is run, you first access its data by using getRootField just like you are doing. So, if I have a mutation like:
mutation UserAddMutation($input: UserAddInput!) {
UserAdd(input: $input) {
userEdge {
node {
name
id
age
}
}
error
}
}
You will do:
const newEdge = store.getRootField('UserAdd').getLinkedRecord('userEdge');
connectionUpdater({
store,
parentId: ROOT_ID,
connectionName: 'UserAdd_users',
edge: newEdge,
before: true,
});
This connectionUpdater is a helper function that looks likes this:
export function connectionUpdater({ store, parentId, connectionName, edge, before, filters }) {
if (edge) {
if (!parentId) {
// eslint-disable-next-line
console.log('maybe you forgot to pass a parentId: ');
return;
}
const parentProxy = store.get(parentId);
const connection = ConnectionHandler.getConnection(parentProxy, connectionName, filters);
if (!connection) {
// eslint-disable-next-line
console.log('maybe this connection is not in relay store yet:', connectionName);
return;
}
const newEndCursorOffset = connection.getValue('endCursorOffset');
connection.setValue(newEndCursorOffset + 1, 'endCursorOffset');
const newCount = connection.getValue('count');
connection.setValue(newCount + 1, 'count');
if (before) {
ConnectionHandler.insertEdgeBefore(connection, edge);
} else {
ConnectionHandler.insertEdgeAfter(connection, edge);
}
}
}
Hope it helps :)

Cancel local requests on component destroy

Consider you have some component e.g. autocomplete that sends GET request to server:
...
someObject = create();
vm.search = someFactory.getHttp(params).then(result => {
someObjet.prop = result;
});
vm.$onDestroy = () => {
someObject = null;
}
If component is destroyed while request is pending, callback will throw js error.
I know that in this concrete example I can solve this using simple If, however quite obvious that it is better to canel this request:
var canceler = $q.defer();
vm.search = someFactory.getHttp(params, canceler)...
vm.$onDestroy = () => {
canceler.resolve();
someObject = null;
}
This works perfectly, but having such code in each component seems weird. I would like to have something like:
vm.search = someFactory.getHttp(params, $scope.destroyPromise)
But such thing does not seem to exist...
Question: Is there any easy way to cancel requests on component destroy?
both in Angularjs or in Angular
One thing I've done in Angular is use RXjs for requests, using subscriptions, and adding those subscriptions to an array that we iterate over and cancel on destroy, like this:
import { Component, OnInit, OnDestroy} from "#angular/core";
import { ActivatedRoute } from "#angular/router";
export class AppComponent implements OnInit, OnDestroy {
private subscriptions = [];
constructor(private route AppRoute) {}
ngOnInit() {
this.subscriptions.push(this.route.data.subscribe());
}
ngOnDestroy() {
for (let subscription of this.subscriptions) {
subscription.unsubscribe();
}
}
}
Why not assign the created object to an object that doesn't get destroyed? This way your behaviour is maintained and the you can do a simple check to prevent any errors.
...
var baseObj = {};
baseObj.someObject = create();
vm.search = someFactory.getHttp(params).then(result => {
if (baseObj.someObject != null) {
baseObj.someObjet.prop = result;
}
});
vm.$onDestroy = () => {
baseObj.someObject = null;
}

Trigger set() when adding element to array

I am attempting to manage an array within an Angular service like so:
import { TodoItem } from '../models/todo-item.model';
#Injectable()
export class TodoService {
//local storage key name
private readonly lsKey = 'pi-todo';
private _todos: Array<TodoItem>;
//Gets the todo items from local storage
public fetchTodos(): Array<TodoItem> {
//Either get the items if they exist, or get an empty array
this.todos = (JSON.parse(localStorage.getItem(this.lsKey)) as Array<TodoItem>) || [];
return this.todos;
}
//Adds the todo item to local storage
public addTodo(todo: TodoItem): Array<TodoItem> {
if (todo) {
//Better way to do this?
let tempTodos = this.todos;
tempTodos.push(
Object.assign(
{
completed: false
},
todo
)
);
this.todos = tempTodos;
return this.todos;
}
}
private get todos(): Array<TodoItem> {
return this._todos || [];
}
private set todos(todos: Array<TodoItem>) {
this._todos = todos;
localStorage.setItem(this.lsKey, JSON.stringify(this._todos));
}
}
When adding a todo item to the todos array, I tried doing this.todos.push(...); but then that doesn't trigger the setter. How can I do this without using a temp array?
I'd suggest moving the "save to local storage" code to a separate method called by both the setter and the add.
//Adds the todo item to local storage
public addTodo(todo: TodoItem): Array<TodoItem> {
if (todo) {
this.todos.push(
Object.assign(
{
completed: false
},
todo
)
);
this.save();
return this.todos;
}
}
private set todos(todos: Array<TodoItem>) {
this._todos = todos;
this.save();
}
private save() {
localStorage.setItem(this.lsKey, JSON.stringify(this._todos));
}
Yes, because you aren't setting it a new value. A work around would be this: instead of pushing into the array, grab the current array, assign it to a temp variable, then replace with new array. Like so:
triggerSet(newValue) {
const tempArray = this.todos;
tempArray.push(newValue);
this.todos = tempArray;
}

Resources