I have a subtitle file, structured as follows:
{ id: '54',
startTime: '00:03:46,572',
endTime: '00:03:53,160',
text: 'Hello this is a text'
},
{ id: '55',
startTime: '00:03:53,160',
endTime: '00:03:58,799',
text: 'To be precise, the subtitle file of a movie'
},
I would now like to go through this file and show the subtitles whenever the respective time is reached in a playing video. I do know roughly how I would realise something like this in javascript, but I am wondering how to do it using React & Redux.
Could I somehow save the time of the video playing in my state and then, everytime that changes, react accordingly with my subtitles? Or what would you suggest? I would very much welcome some code / examples.
This question feel way too general.
The way I would approach the problem is following. Hopefully it gives you a general idea.
I presume subtitles is an array of objects and ids correspond to array indexes.
const initialState = {
activeSubtitleId: 0,
// Here are your subtitles
subtitles: [],
}
Then setup a reducer listening to one action
export const videoReducer = (state = initialState, action) => {
switch(action.type) {
case SYNC_SUBTITLES: {
const { videoTime } = action.payload
const activeSubtitleId = state.subtitles
.filter(subtitle => subtitle.startTime >= videoTime && subtitle.endTime <= videoTime)[0].id
return { ...state, activeSubtitleId }
}
}
}
export default videoReducer
Last missing piece is an action SYNC_SUBTITLES
export const SYNC_SUBTITLES = "SYNC_SUBTITLES"
export const syncSubtitles = (videoTime) => ({
type: SYNC_SUBTITLES,
payload: {
videoTime
}
})
To wire it all up you need to connect redux store to your component, and extract state.subtitles[state.activeSubtitleId]. You might some memoizing library for this, for example Reselect. Depending of how often will you like to sample the time.
Now you can display the correct subtitles.
To keep them synced, you need to dispatch SYNC_SUBTITLES action from your component with defined tick frequency. You can use setTimeout for this, or built-in videoplayer tick emitter.
So the connect might look like this. Pretty silly, but working.
#connect((state, props) => {
const activeSubtitleId = state.activeSubtitleId;
const subtitles = state.subtitles;
return {
activeSubtitle: subtitles[activeSubtitleId]
}
}, {
syncSubtitles
})
Related
I feel like I'm missing something obvious. I have IDs stored as [String] that I want to be able to resolve to the full objects they represent.
Background
This is what I want to enable. The missing ingredient is the resolvers:
const bookstore = `
type Author {
id: ID!
books: [Book]
}
type Book {
id: ID!
title: String
}
type Query {
getAuthor(id: ID!): Author
}
`;
const my_query = `
query {
getAuthor(id: 1) {
books { /* <-- should resolve bookIds to actual books I can query */
title
}
}
}
`;
const REAL_AUTHOR_DATA = [
{
id: 1,
books: ['a', 'b'],
},
];
const REAL_BOOK_DATA = [
{
id: 'a',
title: 'First Book',
},
{
id: 'b',
title: 'Second Book',
},
];
Desired result
I want to be able to drop a [Book] in the SCHEMA anywhere a [String] exists in the DATA and have Books load themselves from those Strings. Something like this:
const resolve = {
Book: id => fetchToJson(`/some/external/api/${id}`),
};
What I've Tried
This resolver does nothing, the console.log doesn't even get called
const resolve = {
Book(...args) {
console.log(args);
}
}
HOWEVER, this does get some results...
const resolve = {
Book: {
id(id) {
console.log(id)
return id;
}
}
}
Where the console.log does emit 'a' and 'b'. But I obviously can't scale that up to X number of fields and that'd be ridiculous.
What my team currently does is tackle it from the parent:
const resolve = {
Author: {
books: ({ books }) => books.map(id => fetchBookById(id)),
}
}
This isn't ideal because maybe I have a type Publisher { books: [Book]} or a type User { favoriteBooks: [Book] } or a type Bookstore { newBooks: [Book] }. In each of these cases, the data under the hood is actually [String] and I do not want to have to repeat this code:
const resolve = {
X: {
books: ({ books }) => books.map(id => fetchBookById(id)),
}
};
The fact that defining the Book.id resolver lead to console.log actually firing is making me think this should be possible, but I'm not finding my answer anywhere online and this seems like it'd be a pretty common use case, but I'm not finding implementation details anywhere.
What I've Investigated
Schema Directives seems like overkill to get what I want, and I just want to be able to plug [Books] anywhere a [String] actually exists in the data without having to do [Books] #rest('/external/api') in every single place.
Schema Delegation. In my use case, making Books publicly queryable isn't really appropriate and just clutters my Public schema with unused Queries.
Thanks for reading this far. Hopefully there's a simple solution I'm overlooking. If not, then GQL why are you like this...
If it helps, you can think of this way: types describe the kind of data returned in the response, while fields describe the actual value of the data. With this in mind, only a field can have a resolver (i.e. a function to tell it what kind of value to resolve to). A resolver for a type doesn't make sense in GraphQL.
So, you can either:
1. Deal with the repetition. Even if you have ten different types that all have a books field that needs to be resolved the same way, it doesn't have to be a big deal. Obviously in a production app, you wouldn't be storing your data in a variable and your code would be potentially more complex. However, the common logic can easily be extracted into a function that can be reused across multiple resolvers:
const mapIdsToBooks = ({ books }) => books.map(id => fetchBookById(id))
const resolvers = {
Author: {
books: mapIdsToBooks,
},
Library: {
books: mapIdsToBooks,
}
}
2. Fetch all the data at the root level instead. Rather than writing a separate resolver for the books field, you can return the author along with their books inside the getAuthor resolver:
function resolve(root, args) {
const author = REAL_AUTHOR_DATA.find(row => row.id === args.id)
if (!author) {
return null
}
return {
...author,
books: author.books.map(id => fetchBookById(id)),
}
}
When dealing with databases, this is often the better approach anyway because it reduces the number of requests you make to the database. However, if you're wrapping an existing API (which is what it sounds like you're doing), you won't really gain anything by going this route.
I tried searching a lot of internet but could not find answer to a simple question. I am very new to mithril (do not know why people chose mithril for project :( ). I want to iterate through a list of strings and use its value in drop down with a checkbox.
const DefaultListView = {
view(ctrl, args) {
const parentCtrl = args.parentCtrl;
const attr = args.attr;
const cssClass = args.cssClass;
const filterOptions = ['Pending', 'Paid', 'Rejected'];
// as of now, all are isMultipleSelection filter
const selectedValue =
parentCtrl.filterModel.getSelectedFilterValue(attr);
function isOptionSelected(value) {
return selectedValue.indexOf(value) > -1;
}
return m('.filter-dialog__default-attr-listing', {
class: cssClass
}, [
m('.attributes', {
onscroll: () => {
m.redraw(true);
}
}, [
filterOptions.list.map(filter => [
m('.dropdown-item', {
onclick() {
// Todo: Add click metrics.
// To be done at time of backend integration.
document.body.click();
}
}, [
m('input.form-check-input', {
type: 'checkbox',
checked: isOptionSelected(filter)
}),
m('.dropdown-text', 'Pending')
])
])
])
]);
}
};
Not sure. How to do it. This is what I have tried so far but no luck. Can someone help me this?
At the beginning of the view function you define an array:
const filterOptions = ['Pending', 'Paid', 'Rejected'];
But later on in the view code where you perform the list iteration, filterOptions is expected to be an object with a list property:
filterOptions.list.map(filter =>
That should be filterOptions.map(filter =>.
There may be other issues with your code but it's impossible to tell without seeing the containing component which passes down the args. You might find it more helpful to ask the Mithril chatroom, where myself and plenty of others are available to discuss & assist with tricky situations.
I'm very new to Redux, and confused as to how to update nested state.
const initialState = {
feature: '',
scenarios: [{
description: '',
steps: []
}]
}
I know that to just push to an array in an immutable way, we could do,
state = {
scenarios: [...state.scenarios, action.payload]
}
And to push into a specific attribute, as this SO answer suggests
How to access array (object) elements in redux state, we could do
state.scenarios[0].description = action.payload
But my question is, how would we access a particular attribute in an object array without mentioning the index? is there a way for us to push it to the last empty element?
All suggestions are welcome to help me understand, thank you in advance :)
Redux helps to decouple your state transformations and the way you render your data.
Modifying your array only happens inside your reducers. To specify which scenario's description you want to modify is easy to achieve by using an identifier. If your scenario has in id, it should be included in your action, e.g.
{
"type": "update_scenario_description",
"payload": {
"scenario": "your-id",
"description": "New content here"
}
}
You can have a reducer per scenario. The higher level reducer for all scenarios can forward the action to the specific reducer based on the scenario id, so that only this scenario will be updated.
In your ui, you can use the array of scenarios and your scenario id to render only the specific one you're currently viewing.
For a more detailed explanation, have a look at the todo example. This is basically the same, as each todo has an id, you have one reducer for all todos and a specific reducer per todo, which is handled by it's id.
In addition to the accepted answer, I'd like to mention something in case someone still wants to "access a particular attribute in an object array without mentioning the index".
'use strict'
const initialState = {
feature: '',
scenarios: [{
description: '',
steps: []
}]
}
let blank = {}
Object.keys(initialState.scenarios[0]).map(scene => {
if (scene === 'steps'){
blank[scene] = [1, 2]
} else {
blank[scene]=initialState.scenarios[0][scene]
}
})
const finalState = {
...initialState,
scenarios: blank
}
console.log(initialState)
console.log(finalState)
However, if scenarios property of initialState instead of being an object inside an array, had it been a simple object like scenarios:{description:'', steps: []}, the solution would have been much simpler:
'use strict'
const initialState = {
feature: '',
scenarios: {
description: '',
steps: []
}
}
const finalState = {
...initialState,
scenarios: {
...initialState.scenarios, steps: [1, 2, 4]
}
}
console.log(initialState)
console.log(finalState)
My question is a continuation of this excellent question and answer concerning the shape of paginated data in a redux store. I am using ngrx/store in an angular 2 app.
{
entities: {
users: {
1: { id: 1, name: 'Dan' },
42: { id: 42, name: 'Mary' }
}
},
visibleUsers: {
ids: [1, 42],
isFetching: false,
offset: 0
}
}
Based on the above shape I believe if the offset (or page, sort, etc.) from an incoming request payload changed then the visible users would change as well as the user entities by calling the DB. I have some actions and reducer functions to handle this and it works as expected. If the offset remains the same and the user is returning to the page the way they left it then the user entities should be returned by the store not the DB.
Where I am struggling is where to put that logic and which rxjs operators to use (still learning this).
I think the correct place is an effect. Here is what I have now in my angular2 app (I am injecting Actions, Store, and my UserService) that pulls new data every time the page is loaded.
#Effect loadUsers$ = this.actions$
.ofType('LOAD_USERS')
.switchMap(() => this.userService.query()
.map((results) => {
return new LoadUsersSuccessAction(results);
}))
.catch(() => Observable.of(new LoadUsersFailureAction()));
My best idea is something like this:
#Effect loadUsers$ = this.actions$
.ofType('LOAD_USERS')
.withLatestFrom(this.store.select(state => state.visibleUsers.offset))
.switchMap(([action, state]) => {
//something that looks like this??
//this syntax is wrong and I can't figure out how to access the action payload
state.offset === payload.offset
? this.store.select(state => state.entities.users)
: this.userService.query()
}
.map((results) => {
return new LoadUsersSuccessAction(results);
}))
.catch(() => Observable.of(new LoadUsersFailureAction()));
Not sure how to make this work. Thanks ahead.
I don't like answering my own questions but it took me quite a while to find the answer to this. I won't accept this answer as I am not sure this is the best way to go about things (still learning the ins/outs). It does however, work perfectly.
I found the correct syntax in a github gist. This code does not match my example but it clearly demonstrates "2 options for conditional ngrx effects" by either returning a store or api observable using some sort of condition.
Hope this helps someone.
Here it is:
#Effect()
selectAndLoadStore$: Observable<Action> = this.actions$
.ofType(storeActions.SELECT_AND_LOAD_STORE)
.withLatestFrom(this.store.select(ngrx.storeState))
.map(([action, storeState]) => [action.payload, storeState])
.switchMap(([storeName, storeState]) => {
const existsInStore = Boolean(storeState.urlNameMap[storeName]);
return Observable.if(
() => existsInStore,
Observable.of(new storeActions.SetSelectedStore(storeName)),
this.storeService.getByUrlName(storeName)
.map(store => new storeActions.LoadSelectedStoreSuccess(store))
);
});
#Effect()
selectAndLoadStore$: Observable<Action> = this.actions$
.ofType(storeActions.SELECT_AND_LOAD_STORE)
.withLatestFrom(this.store.select(ngrx.storeState))
.map(([action, storeState]) => [action.payload, storeState])
.switchMap(([storeName, storeState]) => {
const existsInStore = Boolean(storeState.urlNameMap[storeName]);
let obs;
if (existsInStore) {
obs = Observable.of(new storeActions.SetSelectedStore(storeName));
} else {
obs = this.storeService.getByUrlName(storeName)
.map(store => new storeActions.LoadSelectedStoreSuccess(store));
}
return obs;
});
Imagine a UI with two React components:
<FilterContainer />
<UserListContainer />
We pull down an array of users from the server:
[
{
name: 'John',
enjoys: ['Sailing', 'Running']
},
{
name: 'Bob',
enjoys: ['Running', 'Eating']
},
{
name: 'Frank',
enjoys: ['Sailing', 'Eating']
}
]
The UI looks a little like this:
Filter: Sailing Running Eating
UserList:
John
Frank
You can click on either a filter or a user. To get to this stage, we've clicked on 'Sailing' and then on 'Frank' (maybe we see a nice photo of Frank in the middle of the screen).
My Redux state, built using combineReducers, looks like this:
{
ui: {
filter: {enjoys: 'Sailing'},
userList: {selected: 'John'}
}
data: [user array]
}
I have two actions, SELECT_USER and SELECT_FILTER.
When I click on a filter (SELECT_FILTER fires), I want the ui.userList.selected to persist if that user is still in the filter, and the ui.userList.selected to be set to null if the user is not in the filter.
So if I now click on Eating, I'll see a list with Bob and Frank in it, and Frank is selected. But if I click on Running, I'll see John and Bob, but neither are selected.
However I'm struggling to do this in the conventional Redux methodology. When the userList reducer sees the SELECT_FILTER action, there's no way for it to check the data state to see if the currently selected user is still in that filter condition or not.
What's the right way to do this?
function filter(state = {enjoys: null}, action) {
switch (action.type) {
case SELECT_FILTER:
return {
...state,
enjoys: action.enjoys
}
default:
return state
}
}
function userList(state = {selected: null}, action) {
switch (action.type) {
case SELECT_USER:
return {
...state,
selected: action.name
}
default:
return state
}
}
const ui = combineReducers({
filter,
userList
})
let initialUsers = [
{
name: 'John',
enjoys: ['Sailing', 'Running']
},
{
name: 'Bob',
enjoys: ['Running', 'Eating']
},
{
name: 'Frank',
enjoys: ['Sailing', 'Eating']
}
]
const rootReducer = combineReducers({
ui,
data: (state=initialUsers) => state // in reality, loaded from server
})
export default rootReducer
Reducer should be aware only of a small part of state.
Good place for described logic is the action creator. With redux-thunk you will be able to make a decision based on a global state.
function selectFilter(enjoys) {
return (dispatch, getState) => {
dispatch({type: SELECT_FILTER, enjoys});
// if getState().ui.userList.selected exists in getState().data
dispatch({type: SELECT_USER, name: null});
}
};
You need another action for this.
If you are filtering the users in the data reducer, you will need to dispatch an action in one of your components' hooks (componentWillUpdate or componentWillReceiveProps) when you detect that the array of users has changed. This action will provide your filter reducer with the current array of users, and there you can set the selected field as you like.
If you are filtering the users in the server, I guess you already have an action like FETCH_USERS_SUCCESS that you can use for this.
It should be handled by the filter reducer. You need to send the users data as part of the action payload. Hence the reducer could Calc the selected user logic. You should consider adding a new action, as #cuttals suggested.