Vue Element.ui tree, emit reload event - javascript

I create a SPA on Vue (something like a cloud disk), using the Element.ui tree component, to display the folder tree. The problem is that the tree itself does not load everything at once, but is loaded sequentially with the help of the lazy modifier.
The server part is mongoose + mongoose-path-tree. The use of the mongoose-path-tree means, that a tree storage scheme in the database is next: each node does not know about its children, but the children store their full path to the root element:
#root_id #subroot_id # ... #parent_id #this_id
The problem is that if the user makes changes to the tree (loads the file, creates a new folder, etc.) and the server accepts them, then how to notify the tree that it needs to load new data. Element ui in its documentation does not describe how to capture an event to cause the redrawing of a tree.
Client tree template
<el-tree
:props="defaultProps"
:load="loadNode"
#node-click="handleNodeClick"
:expand-on-click-node="false"
lazy
node-key="id"
ref="tree"
/>
Loading new nodes
loadNode: async function (node, resolve) {
try {
let firstGeneration = await foldersAPI.get(this.$store.state.user.myDrive);
if (node.level === 0) {
return resolve(firstGeneration.data.folder.children);
} else {
var data;
if (node.data.hasChildren) {
let children = await foldersAPI.get(node.data._id);
console.log(children);
data = children.data.folder.children;
} else {
data = [];
}
resolve(data);
}
} catch (e) {
console.log(e)
}

My solution was to simply rerender the treeview by adding the v-if attribute to the treeview and then creating a method
reload() {
this.show = false;
this.$nextTick(() => {
this.show = true
})
}
Calling that reload function then properly reloads the treeview

Try to pass computed variable as prop for tree component. All changes in computed will trigger rerender of tree.

you can set a key property to the component, and then, just change the key value, the component will re-render.
<el-tree
:props="defaultProps"
:load="loadNode"
#node-click="handleNodeClick"
:expand-on-click-node="false"
lazy
node-key="id"
ref="tree"
:key="myKey"
/>
read the doc for more details: https://v2.vuejs.org/v2/guide/conditional.html#Controlling-Reusable-Elements-with-key

Related

How to write data to provide/inject asynchronously?

I have a page on which data is initialized via async fetch:
async fetch() {
const res = await requestApi(this, '/database');
this.sliderData = res.homeSlider;
this.modelData = res.model;
...
}
Then this data is thrown into the child components through the props, but since there are child components of level 3-4, it becomes not convenient to use the props with page. Is it possible to use provide/inject in this case? Moreover, it is important that the transmitted data is reactive. Objects always come from the request when trying to use provide/inject:
provide() {
return {
sliderData: this.sliderData
};
}
The data did not have time to be initialized and an empty object was sent.
I'm not sure that this is the best idea, but you can add a variable (for example isLoading: true) to your data; when all data you need is loaded change this.isLoading = false; additionally, add v-if="!isLoading" to the parent component. In this case, your child component will inject already loaded sliderData.

Vue child component not displaying dynamic data on first page load

Given the code below, my child component alerts trigger before any of the code in the Parent mounted function.
As a result it appears the child has already finished initialization before the data is ready and therefor won't display the data until it is reloaded.
The data itself comes back fine from the API as the raw JSON displays inside the v-card in the layout.
My question is how can I make sure the data requested in the Parent is ready BEFORE the child component loads? Anything I have found focuses on static data passed in using props, but it seems this completely fails when the data must be fetched first.
Inside the mounted() of the Parent I have this code which is retrieves the data.
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray).then(() => {
console.log('DATA ...') // fires after the log in Notes component
this.checkAttendanceForPreviousTwoWeeks().then(()=>{
this.getCurrentParticipants().then((results) => {
this.currentP = results
this.notesArr = this.notes // see getter below
})
The getter that retrieves the data in the parent
get notes() {
const newNotes = eventsModule.getNotes
return newNotes
}
My component in the parent template:
<v-card light elevation="">
{{ notes }} // Raw JSON displays correctly here
// Passing the dynamic data to the component via prop
<Notes v-if="notes.length" :notesArr="notes"/>
</v-card>
The Child component:
...
// Pickingn up prop passed to child
#Prop({ type: Array, required: true })
notesArr!: object[]
constructor()
{
super();
alert(`Notes : ${this.notesArr}`) // nothing here
this.getNotes(this.notesArr)
}
async getNotes(eventNotes){
// THIS ALERT FIRES BEFORE PROMISES IN PARENT ARE COMPLETED
alert(`Notes.getNotes CALL.. ${eventNotes}`) // eventNotes = undefined
this.eventChanges = await eventNotes.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
...
I am relatively new to Vue so forgive me if I am overlooking something basic. I have been trying to fix it for a couple of days now and can't figure it out so any help is much appreciated!
If you are new to Vue, I really recommend reading the entire documentation of it and the tools you are using - vue-class-component (which is Vue plugin adding API for declaring Vue components as classes)
Caveats of Class Component - Always use lifecycle hooks instead of constructor
So instead of using constructor() you should move your code to created() lifecycle hook
This should be enough to fix your code in this case BUT only because the usage of the Notes component is guarded by v-if="notes.length" in the Parent - the component will get created only after notes is not empty array
This is not enough in many cases!
created() lifecycle hook (and data() function/hook) is executed only once for each component. The code inside is one time initialization. So when/if parent component changes the content of notesArr prop (sometimes in the future), the eventChanges will not get updated. Even if you know that parent will never update the prop, note that for performance reasons Vue tend to reuse existing component instances when possible when rendering lists with v-for or switching between components of the same type with v-if/v-else - instead of destroying existing and creating new components, Vue just updates the props. App suddenly looks broken for no reason...
This is a mistake many unexperienced users do. You can find many questions here on SO like "my component is not reactive" or "how to force my component re-render" with many answers suggesting using :key hack or using a watcher ....which sometimes work but is almost always much more complicated then the right solution
Right solution is to write your components (if you can - sometimes it is not possible) as pure components (article is for React but the principles still apply). Very important tool for achieving this in Vue are computed propeties
So instead of introducing eventChanges data property (which might or might not be reactive - this is not clear from your code), you should make it computed property which is using notesArr prop directly:
get eventChanges() {
return this.notesArr.map(note => {
return {
eventInfo: {
name: note.name,
group: note.groupNo || null,
date: note.displayDate,
},
note: note.noteToPresenter
}
})
}
Now whenever notesArr prop is changed by the parent, eventChanges is updated and the component will re-render
Notes:
You are overusing async. Your getNotes function does not execute any asynchronous code so just remove it.
also do not mix async and then - it is confusing
Either:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
await Promise.all(promisesArray)
await this.checkAttendanceForPreviousTwoWeeks()
const results = await this.getCurrentParticipants()
this.currentP = results
this.notesArr = this.notes
or:
const promisesArray = [this.loadPrivate(),this.loadPublic()]
Promise.all(promisesArray)
.then(() => this.checkAttendanceForPreviousTwoWeeks())
.then(() => this.getCurrentParticipants())
.then((results) => {
this.currentP = results
this.notesArr = this.notes
})
Great learning resource

Is it possible to use references as react component's prop or state?

I want to create react table component which values are derived from single array object. Is it possible to control the component from view side? My goal is that every user using this component in their web browsers share the same data via singleton view object.
Program modeling is like below.
Database - there are single database in server which contain extinct and independent values.
DataView - there are singleton View class which reflects Database's table and additional dependent data like (sum, average)
Table - I'll build react component which looks like table. And it will show View's data with supporting sorting, filtering, editing and deleting row(s) feature (and more). Also it dose not have actual data, only have reference of data from View(Via shallow copy -- This is my question, is it possible?)
My intentions are,
- When user changes value from table, it is queried to DB by View, and if succeed, View will refer updated data and change it's value to new value and notify to Table to redraw it's contents. -- I mean redraw, not updating value and redraw.
- When values in View are changed with DB interaction by user request, there are no need to update component's value cause the components actually dose not have values, only have references to values (Like C's pointer). So only View should do is just say to Component to redraw it's contents.
I heard that React's component prop should be immutable. (Otherwise, state is mutable) My goal is storing references to component's real value to it's props so that there are no additional operation for reflecting View's data into Table.
It is concept problems, and I wonder if it is possible. Since javascript dose not support pointer officially(Am I right?), I'm not sure if it is possible.
View class is like below,
const db_pool = require('instantiated-singleton-db-pool-interface')
class DataView {
constructor() {
this.sessions = ['user1', 'user2'] // Managing current user who see the table
this.data = [ // This is View's data
{id:1, name:'James', phone:'12345678', bank:2000, cash:300, total:2300,..},
{id:2, name:'Michael', phone:'56785678', bank:2500, cash:250, total:2300,..},
{id:3, name:'Tyson', phone:'23455432', bank:2000, cash:50, total:2300,..}
] // Note that 'total' is not in db, it is calculated --`dependent data`
}
notifySessionToUpdate(ids) {
// ids : list of data id need to be updated
this.sessions.forEach((session) => {
session.onNotifiedUpdateRow(ids) // Call each sessions's
})
}
requestUpdateRow(row, changed_value) {
// I didn't write async, exception related code in this function for simple to see.
update_result = db_pool.update('UPDATE myTable set bank=2500 where id=1')
if (update_result === 'fail') return; // Do Nothing
select_result = db_pool.select('SELECT * from myTable where id=1') // Retrieve updated single data which object scheme is identical with this.data's data
for (k in Object.keys(select_result)) {.ASSIGN_TO_row_IF_VALUE_ARE_DIFFERENT.} // I'm not sure if it is possible in shallow copy way either.
calc.reCalculateRow(row) // Return nothing just recalculate dependant value in this.data which is updated right above.
// Notify to session
this.notifySessionToUpdate([1]) // Each component will update table if user are singing id=1's data if not seeing, it will not. [1] means id:1 data.
return // Success
}
... // other View features
}
Regarding session part, I'm checking how to implement sessionizing(?) the each user and it's component who is communicating with server. So I cannot provide further codes about that. Sorry. I'm considering implementing another shallow copied UserView between React Component Table and DataView(And also I think it helps to do something with user contents infos like sorting preference and etc...)
Regarding DB code, it is class which nest it's pool and query interface.
My problem is that I'm not familiar with javascript. So I'm not sure shallow copy is actually implementable in all cases which I confront with.
I need to think about,
1. Dose javascript fully support shallowcopy in consistent way? I mean like pointer, guarantee check value is reference or not.
2. Dose react's component can be used like this way? Whether using props or state Can this be fullfilled?
Actually, I strongly it is not possible to do that. But I want to check your opinions. Seems it is so C language-like way of thinking.
Redraw mean re-render. You can expose setState() or dispatch() functions from Table component and call them on View level using refs:
function View() {
const ref = useRef();
const onDbResponse = data => ref.current.update(data);
return (
<Table ref={ ref } />
);
}
const Table = React.forwardRef((props, ref) => {
const [ data, setData ] = useState([]);
useImperativeHandler(ref, {
update: setData
});
...
});
Anyway i don't think it's a good practice to update like that. Why can't you just put your data in some global context and use there?
const Context = React.createContext({ value: null, query: () => {} });
const Provider = ({ children }) => {
const [ value, setValue ] = useState();
const query = useCallback(async (request) => {
setValue(await DB.request(request));
}, [ DB ]);
const context = { value, query };
return <Context.Provider value={ context }>{ children }</Context.Provider>;
}
const useDB = () => useContext(Context);
const View = () => {
const { request } = useDB();
request(...);
}
const Table = () => {
const { value } = useDB();
...
}

Appending non-React DOM node in React component?

Google Map nodes do not deal well with being removed from the DOM and everything following is trying to deal with that.
Up until now I've created a map node within a React component at the highest level in my app and never unmounted it. I'm using a bunch of convoluted css to show/hide/position this node based on what route I'm on and also using a lot of logic in the compenentWillReceiveUpdate() lifecycle method. I really want to use a Google Map within normal mounted/unmounted components and have it nested in the DOM tree where I intend it to be.
I'm playing with placing a <div id='my-google-map> node on <body> and pulling it into a component in componentDidMount() and placing it back on <body> when unmounting. Here's what I'm currently working with.
componentDidMount: function () {
const { mapWrapper } = this.refs
let mapNode = document.getElementById('my-google-map')
if (!mapNode) {
mapNode = document.createElement('div')
mapNode.setAttribute('id', 'my-google-map')
}
mapWrapper.appendChild(mapNode)
this.setState({
mapNode: mapNode
})
},
componentWillUnmount: function () {
document.body.appendChild(this.state.mapNode)
},
render: function () {
return (
<div ref='mapWrapper'>
<span>{this.props.currentDropdown}</span>
</div>
)
}
This seems to be fine and re-renders look like they just ignore the appended node but this feels weird and I'm worried I'm missing something.
Will/when will a future render delete the appended node? Should I just return false from shouldComponentUpdate() to prevent that?
Is there a better way to have a persistent non-React node within React?

How to update Baobab leaf data before React component render?

ReactJS, Baobab, Material-UI app displays some items, identified by their numeric id. To display those, title and image url's are retrieved from a remote service via ajax. Tree branch stores that data:
data: {
12345: {title:'ABC', image:'https://...'}, // id is 12345
12346: {...
}
Upon item component creation and first rendering, its extended data may, or may not be already available in the tree. If its not, ajax call is enqueued to receive that data. It might happen that multiple items are created with the same item id.
To avoid extra requests for the same id, I want to put a dummy info {title:'loading', image:'spinner.gif'} into the tree upon the first request to that id's info. Thus this data will be used for the very first render(). Successive components would get that dummy info, and will not initiate any extra requests.
Question: how, and where can I place the code to test if the tree has no info yet and place the dummy there to indicate its "penging" state and enqueue the request?
Tried so far:
component's constructor – props are not set there yet;
componentWillMount() – the first render started with the old state of the tree, despite the tree.commit() after setting the dummy value;
in the branch function that dynamically creates components cursor pointing to its data. Got warning:
setState(...): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
This can be solved one level up – once the list of ids is available. But it feels right that a component should be able to handle its data within itself.
Please advice a correct way to immediately update Baobab tree data before the first render of a React Component, from within that Component?
In my case (i am use same stack) wrap branch work fine.
import BaobabPropTypes from 'baobab-react/prop-types';
class Actions {
/**
* #param {Baobab} tree
*/
static prefetchTree = (tree) => {
tree.select(somePath).set(defaultValue);
tree.commit();
};
}
class Page extends React.Component {
static contextTypes = {
tree: BaobabPropTypes.baobab
};
componentWillMount() {
Actions.prefetchTree(this.context.tree);
}
render() {
return <Branch {...this.props}/>;
}
}
Baobab has a get event, use it to detect requests that return values that are not fetched yet:
tree.on('get', function(e) {
if (e.data.data === undefined) {
const path = e.data.path; // requested cursor path like ['data',12345]
const id = path[1];
FETCH_DATA(id)
.then( data => tree.set(path , data) );
tree.set(path, PLACEHOLDER_DATA);
}

Categories