Best way to call tippy() after new elements are loaded to the DOM through Livewire/Alpine.js - laravel-livewire

I am currently calling tippy on x-init in my body tag to get tippy initiated the first time:
<body x-data x-init="tippy('[data-tippy-content]')">
When Livewire adds/removes elements to the DOM, I need to call tippy() again to catch any new tags.
As per Livewire docs, I can hook into new elements being initalised like so:
document.addEventListener("DOMContentLoaded", () => {
Livewire.hook('component.initialized', (component) => {})
Livewire.hook('element.initialized', (el, component) => {
el.dispatchEvent(new Event("livewire-element-initialized"));
})
Livewire.hook('element.updating', (fromEl, toEl, component) => {})
Livewire.hook('element.updated', (el, component) => {})
Livewire.hook('element.removed', (el, component) => {})
Livewire.hook('message.sent', (message, component) => {})
Livewire.hook('message.failed', (message, component) => {})
Livewire.hook('message.received', (message, component) => {})
Livewire.hook('message.processed', (message, component) => {})
});
And then add a listener to my body tag like so:
<body x-data x-on:livewire-element-initialized.window="tippy('[data-tippy-content]')">
While this works, I fear it is sub-optimal, and there are better ways. This event fires many times on every operation. I only need tippy() to be called once per DOM update.

For my use case, this is perfect:
https://alpinejs.dev/component/tooltip

Related

NgRx how to dispatch 2 actions in order

I cant seem to find a way with the NgRx (not RxJS Style) to dispatch 2 Actions in an effect.
I would like to (IN THIS ORDER):
delete a Movie in the Database with an effect,
dispatch deleteMovieSuccess
dispatch getMovies (I need to reload all Movies afterwards!)
I tried to do it like below, but it just fires the first Action, and I cannot see the other action:
In my log I can see:
[Movie list] Delete movie
[Movie list] Get movies
[Movie list] Get movies successful
I have the folloing actions:
export const getMovies = createAction('[Movie List] Get Movies', props<{search: string, page: number, limit: number}>());
export const getMoviesSuccess = createAction('[Movies List] Get Movies Success', props<{ search: string, page: number, limit: number }>());
export const deleteMovie = createAction('[Movie List] Remove Movie', props<{ movieToDelete: Movie, search: string, page: number, limit: number }>());
export const deleteMovieSuccess = createAction('[Movie List] Remove Movie Success', props<{ movieToDelete: Movie }>());
and the following effect:
deleteMovie$ = createEffect(() => this.actions$.pipe(
ofType(MovieActions.deleteMovie),
mergeMap(({movieToDelete, search, page, limit}) =>
this.moviesService.deleteMovie(movieToDelete)
.pipe(
map(() => MovieActions.deleteMovieSuccess({movieToDelete: movieToDelete})),
map(() => MovieActions.getMovies({search, page, limit})),
catchError(() => EMPTY)
)
)
)
);
How can I trigger BOTH, deleteMoviesSuccess, and getMovies in this order?
I`ve also tried with switchMap and and faltMap, but never are both Actions dispatched correctly.
I just cant seem to understand, how dispatching in an iterative way is possible, but I really need it for my special usecase.
Any help is greatly appreciated.
You should not dispatch two action in as a result of an effect. Rather dispatch some kind of success action and then react on that in other effects. Read here
You could create a chain of effect:
dispatch delete action
on finish of delete dispatch deleteSuccess action
on deleteSuccess action trigger loadMovies effect
on loadMovies success some set action for a reducer to pick up
deleteMovie$ = createEffect(() => this.actions$.pipe(
ofType(MovieActions.deleteMovie),
mergeMap(({movieToDelete, search, page, limit}) => this.moviesService.deleteMovie(movieToDelete).pipe(
map(() => MovieActions.deleteMovieSuccess({movieToDelete: movieToDelete})),
catchError(() => EMPTY)
))
));
loadMovie$ = createEffect(() => this.actions$.pipe(
ofType(MovieActions.deleteMovieSuccess),
mergeMap(() => this.moviesService.loadMovies().pipe(
map(movies => MovieActions.setMovies({ movies })),
catchError(() => EMPTY)
))
));
EDIT
Also instead of passing parameters like limit or search you may hold these in the store. Doing so gives you the advantage to always access those in effects when needed. The NgRx Documentation has a great example on how this selecting in an effect is done. ngrx.io/api/effects/concatLatestFrom
you could use the switchMap operator to return an array of actions.
For example, instead of;
deleteMovie$ = createEffect(() => this.actions$.pipe(
ofType(MovieActions.deleteMovie),
mergeMap(({movieToDelete, search, page, limit}) =>
this.moviesService.deleteMovie(movieToDelete)
.pipe(
map(() => MovieActions.deleteMovieSuccess({movieToDelete: movieToDelete})),
map(() => MovieActions.getMovies({search, page, limit})),
catchError(() => EMPTY)
)
)
)
);
You could try;
deleteMovie$ = createEffect(() => this.actions$.pipe(
ofType(MovieActions.deleteMovie),
mergeMap(({movieToDelete, search, page, limit}) =>
this.moviesService.deleteMovie(movieToDelete)
.pipe(
switchMap(() => [MovieActions.deleteMovieSuccess({movieToDelete: movieToDelete}),
MovieActions.getMovies({search, page, limit})]),
catchError(() => EMPTY)
)
)
)
);

How to realize ajax .done in livewire

I want to refresh some html in other container, not where the button is.
In native ajax is like this
.done(function(result){
$('#result').html(result);
});
how it works in livewire alpinejs?
you have 2 option
use Livewire Life Cycle Javascript Hooks
you can see the livewire javascript hooks in this link
Lifecycle Hooks
document.addEventListener("DOMContentLoaded", () => {
Livewire.hook('message.processed', (message, component) => {})
});
Use Browser Events Dispatching
Dispatching Browser Events
you can dispatch a browser event when ever you want in you backend like this
$this->dispatchBrowserEvent('name-updated', ['newName' => $value]);
and for js side you can use this code:
window.addEventListener('name-updated', event => {
alert('Name updated to: ' + event.detail.newName);
})

React | Unit testing how to test methods inside the succesful AJAX call?

Currently, In my react app, I have an ajax call method that has setState if the ajax call is successful, I already mocked the api response, it seems that the method won't execute anything else other than returning the response
For example,
// This is the ajax method
getProductData(productId) {
// I already successfully mocked the response
getProductById(productId).then(productResponse => {
// I am trying make sure that the state is being updated with response data
this.setState({
product: productResponse.data
})
});
}
This is my test
test('Make sure my state is being updated with response data', async () => {
const component = shallow(
<MyComponent />
);
component.instance().getProductData(1);
// This is where my test failed, as it's not updating anything
expect(component.state().product).to.equal('SOMETHING');
});
Tried using setTimeout to asynchronously test the method, suggested by Jonah Pereira
test('Make sure my state is being updated with response data', async () => {
const component = shallow(
<MyComponent />
);
component.instance().getProductData(1);
// Using settimeout still not solving the problem,
setTimeout(() => {
expect(component.state().product).to.equal('SOMETHING');
// Not even this, that means the test totally ignored the setTimeout
expect('abc').to.equal('cba');
}, 0)
});
Usually if I test my react components which involve AJAX calls i enclose my methods inside a setTimeout
setTimeout(() =>
{
expect(component.state().product).to.equal('SOMETHING');
}, 0);

.forEach within async action creator not returning action when running unit test

I am using react/redux to generate a list of panels, each of which displays data on each list item. I set a 5 second interval that calls refreshAppList(this.props.list) action creator that forEach loops through every item in the list and makes an async call which then dispatches the refreshed list item (using redux-thunk). So basically, every 5 seconds I am refreshing the list of panels with the most up-to-date data. This works great! Unfortunately, now that I am writing unit tests for this particular async action creator I have run into an issue. .forEach does not return anything so when I call it in my unit tests I am getting undefined. Does anyone know how to override this issue or maybe i need to use a different method to refresh the entire list of panels?
Here is the action creator that is looping through the array and making an async call on each array item.
export const refreshAppList = list => (dispatch) => {
list.forEach((version, index) => {
const url = `apiEndpoint/${version.data.app_id}/${version.data.version}`;
return axios.get(url)
.then(({ data }) => {
data.uniqueId = version.uniqueId;
data.refreshId = uuidv1();
dispatch({ type: REFRESH_APP_LIST, payload: { index, data } });
})
.catch((e) => {
console.log(e);
});
});
};
Here is the error i am receiving:
1) async actions creates an action with type: REFRESH_APP_LIST:
TypeError: Cannot read property 'then' of undefined
at Context.<anonymous> (tests/asyncActions.js:140:12)
Here is where I am calling the action creator within the test (using redux-mock-store):
return store.dispatch(refreshAppList(list)).then(() => {
expect(store.getActions()).to.deep.equal(expectedActions);
});
I think it is also worth mentioning that I am using axios-mock-adapter to mock the data returned from the async call within the action creator.
One last thing: I have written unit tests for two other async action creators within the same app and both pass. The big difference is that this particular action creator is chaining together multiple async calls using a forEach loop (that is not returning anything to the test).
That doesn't work because the function that refreshAppList returns doesn't return anything. Also, .forEach doesn't return anything even though you do return axios.get. from inside. You could use .map instead and return everything inside Promise.all. Something like this
export const refreshAppList = list => (dispatch) => {
return Promise.all(list.map((version, index) => {
const url = `apiEndpoint/${version.data.app_id}/${version.data.version}`;
return axios.get(url)
.then(({ data }) => {
data.uniqueId = version.uniqueId;
data.refreshId = uuidv1();
dispatch({ type: REFRESH_APP_LIST, payload: { index, data } });
})
.catch((e) => {
console.log(e);
});
}));
};

Enzyme / React Unit test

I have this React functional UI only component, which has two props passed in, the second being a function that is passed from its parent component. The onClick calls 'delegates' to a function in the parent container component, this parent method is then responsible for dispatching to a redux store.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
const BotShowUI = ({ bot, onClick }) => {
return(
<div id={bot.id} onClick={onClick}>
{bot.id} : {bot.text}
</div>
)
}
BotShowUI.propTypes = {
bot: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired
};
export default BotShowUI;
My test spec is, which uses Jasmine
import React, {Component} from 'react';
import { mount } from 'enzyme';
import BotShowUI from '../botShowUI';
function onClickFunction(){};
describe('botShowUI', () => {
const bot = {id: 1, isDone: false, text: 'bot 123'};
const expectedDivText = '1 : bot 123';
const wrapper = mount(<BotShowUI bot={bot} onClick={onClickFunction} />);
it(' div has been rendered ', () => {
expect(wrapper.find('div').first()).not.toBe(null);
});
it(' div displays the correct bot text ', () => {
expect(wrapper.find('div').first().text()).toEqual(expectedDivText)
});
it(' div click event fired ', () => {
wrapper.simulate('click');
expect(wrapper.state('onClick')).toBe(true);
});
});
This last assertion fails with
Chrome 57.0.2987 (Windows 10 0.0.0) botShowUI div click event fired FAILED
TypeError: Cannot read property 'onClick' of null
at ReactWrapper.state (webpack:///~/enzyme/build/ReactWrapper.js:825:24 <- tests.webpack.js:26303:25)
at Object.<anonymous> (webpack:///app/react/components/bots/_tests/botShowUI.spec.js:25:23 <- tests.webpack.js:25415:25)
wrapper.simulate('click'); works, but the next line fails
What is the correct way to assert that the click was fired ?
Do I have to drop into wrapper's props/children instead of using state ?
I'm not trying to test the parent container in any way, the two are isolated.
This test is only concerned with this UI component.
First thing is that onClick isn't on state, but on props, so you will have to access it by doing wrapper.props('onClick').
Secondly, to test whether onClick has been handled or not is to use a spy, rather than an empty function. If you do not want to use spy, you can still do that, but not the way you have done. If you are interested, I can post some pseudo-code for that too. But coming back to using spies, you can use a spy as the onClick prop. Below is the code for that. I have hand-written it, so please check for any syntax error, but you should get the idea on what needs to be done.
it('should call the onClick handler on click', () => {
const onClickFunction = sinon.spy()
wrapper = mount(<BotShowUI bot={bot} onClick={onClickFunction} />)
wrapper.simulate('click');
expect(onClickFunction).toHaveBeenCalled();
})
Based on Abhishek's answer here's my solution for Jasmine
it(' div click event fired ', () => {
let onClickFunction_spy = jasmine.createSpy('onClickFunction');
const wrapper = mount(<BotShowUI bot={bot} onClick={onClickFunction_spy} />);
wrapper.simulate('click');
expect(onClickFunction_spy).toHaveBeenCalled();
});
Hope this helps anyone.