I'm using petite-vue as I need to do very basic UI updates in a webpage and have been drawn to its filesize and simplicity. I'd like to control the UI's state of visible / invisible DOM elements and class names and styles of various elements.
I have multiple JavaScript files in my app, I'd like to be able to make these changes from any of them.
In Vue JS it was possible to do things like this...
const vueApp = new Vue({ el: "#vue-app", data(){
return { count: 1}
}})
setTimeout(() => { vueApp.count = 2 }, 1000)
I'm trying the same with Petite Vue but it does nothing.
// Petite Vue
const petiteVueApp = PetiteVue.createApp({
count: 0,
}).mount("#petite-vue");
setTimeout(() => { petiteVueApp.count = 2 }, 1000);
Logging the app gives just a directive and mount attribute, I can't find the count (nb if you log the above app it will show the count, because of that line petiteVueApp.count = 2, that isn't the data)
Demo:
https://codepen.io/EightArmsHQ/pen/YzemBVB
Can anyone shed any light on this?
There is an example which does exactly this in the docs which I overlooked.
https://github.com/vuejs/petite-vue#global-state-management
It requires an import of the #vue/reactivity which can be imported from the petite-vue bundle.
import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'
setTimeout(() => { vueApp.count = 2 }, 1000)
const store = reactive({
count: 0,
inc() {
this.count++
}
})
createApp({
store
}).mount("#petite-vue")
setTimeout(() => { store.count = 2 }, 1000);
Updated working example:
https://codepen.io/EightArmsHQ/pen/ExEYYXQ
Interesting. Looking at the source code it seems that we would want it to return ctx.scope instead of return this.
Your workaround seems like the best choice if using petite-vue as given, or you could fork petite-vue and change that one line (I haven't tested this).
Related
I'm developing a helpdesk tool in which I have a kanban view.
I previously used nested serializers in my backend and I managed to have everything working with a single query but it's not scalable (and it was ugly) so I switched to another schema :
I query my helpdesk team ('test' in the screenshot)
I query the stages of that team ('new', 'in progress')
I query tickets for each stage in stages
So when I mount my component, I do the following :
async mounted () {
if (this.helpdeskTeamId) {
await this.getTeam(this.helpdeskTeamId)
if (this.team) {
await this.getTeamStages(this.helpdeskTeamId)
if (this.stages) {
for (let stage of this.stages) {
await this.getStageTickets(stage)
}
}
}
}
},
where getTeam, getTeamStages and getStageTickets are :
async getTeam (teamId) {
this.team = await HelpdeskTeamService.getTeam(teamId)
},
async getTeamStages (teamId) {
this.stages = await HelpdeskTeamService.getTeamStages(teamId)
for (let stage of this.stages) {
this.$set(stage, 'tickets', [])
}
},
async getStageTickets (stage) {
const tickets = await HelpdeskTeamService.getTeamStageTickets(this.helpdeskTeamId, stage.id)
// tried many things here below but nothing worked.
// stage.tickets = stage.tickets.splice(0, 0, tickets)
// Even if I try to only put one :
// this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0])
// I see it in the data but It doesn't appear in the view...
// Even replacing the whole stage with its tickets :
// stage.tickets = tickets
// this.stages.splice(this.stages.indexOf(stage), 1, stage)
},
In getTeamStages I add an attribute 'tickets' to every stage to an empty list. The problem is when I query all the tickets for every stage. I know how to insert a single object in an array with splice or how to delete one object from an array but I don't know how to assign a whole array to an attribute of an object that is in an array while triggering the Vue reactivity. Here I'd like to put all the tickets (which is a list), to stage.tickets.
Is it possible to achieve this ?
If not, what is the correct design to achieve something similar ?
Thanks in advance !
EDIT:
It turns out that there was an error generated by the template part. I didn't think it was the root cause since a part of the view was rendered. I thought that it would have prevent the whole view from being rendered if it was the case. But finally, in my template I had a part doing stage.tickets.length which was working when using a single query to populate my view. When making my API more granular and querying tickets independently from stages, there is a moment when stage has no tickets attribute until I set it manually with this.$set(stage, 'tickets', []). Because of that, the template stops rendering and raises an issue. But the ways of updating my stage.tickets would have worked without that template issue.
I could update the stages reactively. Here is my full code; I used the push method of an array object and it works:
<template>
<div>
<li v-for="item in stages" :key="item.stageId">
{{ item }}
</li>
</div>
</template>
<script>
export default {
data() {
return {
stages: [],
};
},
methods: {
async getTeamStages() {
this.stages = [{ stageId: 1 }, { stageId: 2 }];
for (let stage of this.stages) {
this.$set(stage, "tickets", []);
}
for (let stage of this.stages) {
await this.getStageTickets(stage);
}
},
async getStageTickets(stage) {
const tickets = ["a", "b", "c"];
for (let ticket of tickets) {
this.stages[this.stages.indexOf(stage)].tickets.push(ticket);
}
},
},
mounted() {
this.getTeamStages();
},
};
</script>
It should be noted that I used the concat method of an array object and also works:
this.stages[this.stages.indexOf(stage)].tickets = this.stages[this.stages.indexOf(stage)].tickets.concat(tickets);
I tried your approaches some of them work correctly:
NOT WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, tickets)
WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0]);
WORKED
stage.tickets = tickets
this.stages.splice(this.stages.indexOf(stage), 1, stage)
I'm sure it is XY problem..
A possible solution would be to watch the selected team and load the values from there. You seem to be loading everything from the mounted() hook, and I suspect this won't actually load all the content on demand as you'd expect.
I managed to make it work here without needing to resort to $set magic, just the pure old traditional vue magic. Vue will notice the properties of new objects and automatically make then reactive, so if you assign to them later, everything will respond accordingly.
My setup was something like this (showing just the relevant parts) -- typing from memory here, beware of typos:
data(){
teams: [],
teamId: null,
team: null
},
watch:{
teamId(v){
this.refreshTeam(v)
}
},
methods: {
async refreshTeam(id){
let team = await fetchTeam(id)
if(!team) return
//here, vue will auomaticlly make this.team.stages reactive
this.team = {stages:[], ...team}
let stages = await fetchStages(team.id)
if(!stages) return
//since this.team.stages is reactive, vue will update reactivelly
//turning the {tickets} property of each stage reactive also
this.team.stages = stages.map(v => ({tickets:[], ...v}))
for(let stage of this.team.stages){
let tickets = await fetchTickets(stage.id)
if(!tickets) continue
//since tickets is reactive, vue will update it accordingly
stage.tickets = tickets
}
}
},
async mounted(){
this.teams = fetchTeams()
}
Notice that my 'fetchXXX' methods would just return the data retrieved from the server, without trying to actually set the component data
Edit: typos
I'm currently using vue-router to manage the differents Vue of my project.
My main.js
import Vue from 'vue'
import App from './App.vue'
import jQuery from 'jquery'
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.css'
global.jQuery = jQuery
global.$ = jQuery
import './assets/css/animate.css'
import router from './router'
import store from './vuex'
Vue.config.productionTip = false
new Vue({
store,
router,
render: h => h(App)
}).$mount('#app')
When I'm on my dashboard ('/dashboard') for the first time, the 'created' methods is called. Data are retrieved from the API and shows up in my array.
After that I click on one element of my array that route me to '/details/:id' (with id the id of my element). Everything works well and then I click on a 'Go back' button.
I finish again on my dashboard page, i see that the 'create' methods is called again, data are well retrived from the API but nothing shows up and my array stays empty.
I really don't understand why.
There is the code of the the 'created' function:
export default {
created: function() {
console.log('created => dashboard');
let store = this.$store;
let q = this.rows;
//get rows
if (store.state.socket.io._callbacks["$rows"] == undefined) {
console.log("Binding rows");
//Where I receive the rows from API
store.state.socket.io.on("rows", data => {
console.log("rows reponse:", data);
if (data.success) {
this.nbrItems = data.rows.length;
q.splice(0, q.length); //Clean the array without replacing the instance
data.rows.map(a => q.push(a));
console.log("Queue length: " + q.length);
}
});
}
//get the queue
this.refresh(); // This send a request to the API to ask it to send us back the datas
},
And I use this.$router.go(-1) to navigate back on the '/dashboard' page.
Edit: Is there a problem of state or something like that? I do not understand why, because in-memory I can access to all data, there is just no more binding anymore...
Do you pop every element of the array before you call the created function?
I'm still an apprentice but it seems to me like you have to pop everything before adding new elements to the array.
I figure it out:
The problem was coming from socket.io. I'm checking if the event is bind already before to subscribe to a function and this function contains 'this' that was still referring to the previous Vue instance.
Simply fixed by replacing this:
//get rows
if (store.state.socket.io._callbacks["$rows"] == undefined) {
console.log("Binding rows");
//Where I receive the rows from API
store.state.socket.io.on("rows", data => {
console.log("rows reponse:", data);
if (data.success) {
this.nbrItems = data.rows.length;
q.splice(0, q.length); //Clean the array without replacing the instance
data.rows.map(a => q.push(a));
console.log("Queue length: " + q.length);
}
});
}
by this:
if (store.state.socket.io._callbacks["$rows"] != undefined) {
store.state.socket.io.off("rows");
}
console.log("Binding rows");
store.state.socket.io.on("rows", data => {
console.log("rows reponse:", data);
if (data.success) {
this.nbrItems = data.rows.length;
q.splice(0, q.length);
data.rows.map(a => q.push(a));
console.log("Queue length: " + q.length);
}
});
But this makes me wonder, if I can still access to a previous Vue instance is it meaning that it will be some kind of memory leak with time?
I suppose that no with the garbage collector but would mean that nothing else refers to the previous instance.
I’ve just recently started using Vue and so far so good, but I’ve ran into a bit of an issue that I can’t figure out a good solution to.
I have a photo gallery with a few different sections. I have an overall gallery component, a gallery section component and an image component. Essentially, I’m using a photos array for each section to store the photos data for that section. Within the sections I’m using v-for to display the photos. The gallery is infinitely scrolling so when you scroll to the bottom, more images load and the photos array for that section is updated.
Here’s my problem, currently the photos arrays are stored on the data of the overall gallery component, so when I update one of the photos arrays it seems to cause the entire gallery to rerender. The more images on the screen, the worse effect this has on the performance and the less responsive the page becomes.
I’m aware I could move the photos array to the data of the individual sections, but as far as I can tell this would still rerender that entire section.
I don’t really know if there’s any good solution that’ll do what I’m trying to do, having a certain amount of reactivity but only updating the things that changed. I don’t know if something like that is possible.
I’ve tried messing around with computed data, props, methods etc. but I can’t work out a better solution.
Here’s the code I’ve been working with in the overall gallery component:
<template>
<div class="photo-gallery">
<gallery-section v-for="(section, index) in sections" v-bind:section="section" class="photo-gallery__section" v-bind:key="index"></gallery-section>
</div>
</template>
<script>
import * as qb from "../queryBuilder";
let layout = [
{
title: "Highlighted Photos",
request: {
filters: qb.group([
qb.filter("rating", ">=", 4),
]),
options: {
offset: 0,
limit: 2,
order: ["rand()"],
size: 740
}
},
total: 2,
photoClass: "photo--highlighted",
loading: false,
photos: []
},
{
title: "More photos",
request: {
filters: qb.group([
qb.filter("rating", ">=", 2),
]),
options: {
offset: 0,
limit: 40,
order: ["rand()"]
}
},
total: Infinity,
loading: false,
photos: []
}
];
export default {
data() {
return {
sections: layout,
currentSection: 0
}
},
mounted() {
this.getPhotos(this.sections[0]);
this.getPhotos(this.sections[1]);
},
methods: {
getPhotos(section) {
section.loading = true;
let currentSection = this.currentSection;
fetch(`api/photos/search/filter/${JSON.stringify(section.request.filters)}/${JSON.stringify(section.request.options)}`)
.then(response => response.json())
.then(response => {
section.photos.push(...response.images);
// Set offset for next request
this.sections[this.currentSection].request.options.offset = section.photos.length;
// Check if current section is complete or if less than the requested amount of photos was returned
if (
this.sections[this.currentSection].total === section.photos.length ||
response.images.length < this.sections[this.currentSection].request.options.limit
) {
if (this.sections.length -1 != this.currentSection) {
// Move onto next section
this.currentSection++;
} else {
// Set currentSection to null if all sections have been fully loaded
this.currentSection = null;
}
}
})
.finally(() => {
section.loading = false;
});
},
scrollHandler() {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 500) {
if (this.currentSection != null && !this.sections[this.currentSection].loading) {
this.getPhotos(this.sections[this.currentSection]);
}
}
}
},
created() {
window.addEventListener("scroll", this.scrollHandler);
},
destroyed() {
window.removeEventListener("scroll", this.scrollHandler);
}
}
</script>
One thing I've also noticed is that whenever more photos are loaded, the mount function for every photo component on the page runs.
Would anyone be able to point me in the right direction? Any advise would be very much appreciated.
Thank you, Jason.
The issue was the the way I was generating the keys for my photos component instances, which cannot be seen in the code I included above. I figured out that the random number being generated as the key meant Vue could not keep track of the element as the key would keep changing. I'm now generating unique keys on the server side and using them instead. It works as expected now.
I'm trying to make React-based web game. I have an App component which holds pretty much all non-UX state. To avoid code duplication I also hold most functions in it and pass it down as prop to child components.
But now I'm starting to get cluttered by different functions, all in the App body. Is there any simple way to satisfactory structure this in different files? Should I already look into state management libraries?
Currently stuff looks like:
class App extends Component {
constructor(props) {
super(props);
this.state = gameInitialize();
this.modifyState = this.modifyState.bind(this);
this.moveUnit = this.moveUnit.bind(this);
this.progressMission = this.progressMission.bind(this);
this.timeJump = this.timeJump.bind(this);
this.competenceAfterTimeJump = this.competenceAfterTimeJump.bind(this);
this.save = this.save.bind(this);
this.load = this.load.bind(this);
}
componentDidMount() {
this.timerID = setInterval(this.modifyState, this.state.interval);
window.addEventListener('beforeunload', this.save);
this.load();
}
componentWillUnmount() {
clearInterval(this.timerID);
}
save() {
localStorage.setItem("gameSave", toJson(this.state));
}
load() {
let state = 0;
try {
state = fromJson(localStorage.getItem("gameSave"));
} catch (error) {
console.log(error);
return 0;
}
state.units.map(unit => {
delete unit.__parent;
delete unit.attributes.__parent
return 0;
});
state.missions.map(mission => delete mission.__parent);
this.setState(state);
}
modifyState() {
this.setState(this.state.units.map(this.progressMission));
this.setState(this.state);
}
progressMission(unit) {
const mission = unit.currentMission;
let increment = unit.attributes[mission.type].total() - mission.complexity;
if (increment < 0) increment = 0;
mission.progress += increment * this.state.interval / 1000 * unit.competence / 10;
if (mission.progress >= mission.difficulty) {
mission.progress = 0;
this.state.experience.get(mission.reward);
mission.completions += 1;
}
}
moveUnit(unit, mission) {
unit.currentMission = mission;
this.setState(this.state);
}
timeJump() {
const game = this.state;
while (game.units.length > 2) {
game.units.pop();
};
game.units.map(function (unit) {
Object.keys(unit.attributes).map((key) => { unit.attributes[key] = newAttribute() });
unit.currentMission = game.missions[0];
});
game.missions.map((mission) => {mission.progress = 0});
game.units[0].competence = this.competenceAfterTimeJump();
game.experience.current = 0;
this.setState(game);
}
competenceAfterTimeJump() {
return (10 + Math.sqrt(this.state.experience.total) / 10);
}
render() {
return (
<div className="App">
<header className="App-header">
<h1 className="title">Time-traveling Hero: eventually I'll save the world, or maybe not if I don't feel it</h1>
</header>
<SaveLoad game={this} />
<Prestige game={this} />
<MissionWrapper>
<MissionList missions={this.state.missions} game={this} />
</MissionWrapper>
<UnitWrapper>
<ExpWrapper>
<div>
Available Experience: {this.state.experience.current.toFixed(1)}
</div>
<div>
Total Experience: {this.state.experience.total.toFixed(1)}
</div>
</ExpWrapper>
<UnitList units={this.state.units} game={this} />
</UnitWrapper>
</div>
);
}
}
function gameInitialize() {
let game = { units: [], missions: [], currentUnit: undefined };
game.interval = 10;
game.missions = generateMissions(50);
game.experience = {
current: 0, total: 0,
get: function (amount) { this.current += amount; this.total += amount },
spend: function (amount) {
if (this.current >= amount) {
this.current -= amount;
return true;
}
else return false;
}
};
game.units.push({ name: "Hero", attributes: newAttributes(), competence: 10, currentMission: game.missions[0] });
game.units.push({ name: "Childhood Friend", attributes: newAttributes(), competence: 15, currentMission: game.missions[0] });
game.currentUnit = game.units[0];
game.missionsWithUnits = function () {
this.missions.map()
}
return game;
}
How should I proceed?
Yes, it's super easy to organize JS code! Use modules. Here's how to do it.
Export functions from a file
adders.js:
export function addTwo (number) {
return number + 2
}
Then use it:
This could be in a component file:
import { addTwo } from './path/to/adders.js'
console.log(addTwo(5)) // logs 7
You can organize this super well for a lot of things. If you have a group of related functions, use a module like this. here's the file structure:
mathStuff/
adders.js
index.js
You have all of your related files in the same folder and your functions exported from the individual files like above. Then set up index like this:
index.js:
import * as adders from './adders.js'
// Set up your object however you want.
const MathStuff = {
...adders
}
export default MathStuff
Then in any component you can do this:
import MathStuff from './path/to/mathStuff'
MathStuff.addTwo(7) // 9
For even more organization, you could set your index up to have functions like this:
index.js:
import * as adders from './adders.js'
import * as dividers from './dividers.js' // another math file with division functions or something
// Set up your object however you want.
const MathStuff = {
adders,
dividers
}
export default MathStuff
And use it like this:
import MathStuff from './path/to/mathStuff' // points to directory, NOT individual file
MathStuff.adders.addTwo(7) // 9
I would definitely suggest organizing code like this. One thing this improves is testability - it's very easy to test pure functions with no side effects.
I like to put my database code in one module and import it wherever to access all my database functions.
I like to put all of my business logic in different modules by category - for instance GameLogic or something like that.
This will also help you write more functional code. Currently, you have a lot of state modification within individual functions - you won't be able to do that in modules without binding individual functions to the this context of your react component. Instead, I would suggest passing all necessary parameters to the function and having it return a value. This moves business logic away, making it easier to manage state.
For instance, your progressMission function accesses this.state.interval. You can pass interval to the function itself.
One thing I'm noticing is that your code has a lot of dependency on each other - functions often have to access lots of things outside of itself, rather than being self-contained. It would probably help you a lot to try to refactor into a modular system, where functions are much more pure - only accessing what is passed to them, and returning values which get used. Using actual modules like above definitely helps do that - my code got better the more I did it. It helps you reason about your code better. Additionally, once/if you start implementing tests, you'll find that all of the tangled-ness of the code makes it hard to test - there are a lot of side effects.
Finally, redux and external state management probably won't help a ton in your case, but they might. Redux can help you achieve state that's easier to reason about, but it won't help you organize code better per se. I hope that helps!
Problem: ignore some part of the .snap file test results
the question here: there are some components in my test that have a random values and i don't really care about testing them. is there any way to ignore part of my X.snap file? so when i run tests in the future it won't give me test fail results.
Now you can also use property matcher for these cases.
By example to be able to use snapshot with these object :
const obj = {
id: dynamic(),
foo: 'bar',
other: 'value',
val: 1,
};
You can use :
expect(obj).toMatchSnapshot({
id: expect.any(String),
});
Jest will just check that id is a String and will process the other fields in the snapshot as usual.
Actually, you need to mock the moving parts.
As stated in jest docs:
Your tests should be deterministic. That is, running the same tests multiple times on a component that has not changed should produce the same results every time. You're responsible for making sure your generated snapshots do not include platform specific or other non-deterministic data.
If it's something related to time, you could use
Date.now = jest.fn(() => 1482363367071);
I know it's quite old question but I know one more solution. You can modify property you want to ignore, so it will be always constant instead of random / dynamic. This is best for cases when you are using third party code and thus may not be able to control the non deterministic property generation
Example:
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Card from './Card';
import toJSON from 'enzyme-to-json';
Enzyme.configure({ adapter: new Adapter() });
describe('<Card />', () => {
it('renders <Card /> component', () => {
const card = shallow(
<Card
baseChance={1}
name={`test name`}
description={`long description`}
imageURL={'https://d2ph5fj80uercy.cloudfront.net/03/cat1425.jpg'}
id={0}
canBeIgnored={false}
isPassive={false}
/>
);
const snapshot = toJSON(card);
// for some reason snapshot.node.props.style.backgroundColor = "#cfc5f6"
// does not work, seems the prop is being set later
Object.defineProperty(snapshot.node.props.style, 'backgroundColor', { value: "#cfc5f6", writable: false });
// second expect statement is enaugh but this is the prop we care about:
expect(snapshot.node.props.style.backgroundColor).toBe("#cfc5f6");
expect(snapshot).toMatchSnapshot();
});
});
You can ignore some parts in the snapshot tests replacing the properties in the HTML. Using jest with testing-library, it would look something like this:
it('should match snapshot', async () => {
expect(removeUnstableHtmlProperties(await screen.findByTestId('main-container'))).toMatchSnapshot();
});
function removeUnstableHtmlProperties(htmlElement: HTMLElement) {
const domHTML = prettyDOM(htmlElement, Infinity);
if (!domHTML) return undefined;
return domHTML.replace(/id(.*)"(.*)"/g, '');
}
I used this to override moment's fromNow to make my snapshots deterministic:
import moment, {Moment} from "moment";
moment.fn.fromNow = jest.fn(function (this: Moment) {
const withoutSuffix = false;
return this.from(moment("2023-01-12T20:14:00"), withoutSuffix);
});