Skip to main content

2 Multi-User Spreadsheets with ActionCable: Part 3

Episode 139 · September 14, 2016

Build a reactive multi-user spreadsheet web app with ActionCable and RethinkDB

ActionCable


Transcripts

Now the next step will be to stream stream field selections and so this is going to basically say: Let's set the selected cells that we have server side. We added in our user model, we added the selected cell field, and so this is going to update the user with the selected cells, and that's interesting because selected_cells here is plural, and I believe that it was not plural when we saw it up before, so maybe this was supposed to be selected_cells so that we could say that this is an attribute on our users. That's probably the case, so let's go into our ActionCable channels, server side channel and add this method down here at the bottom and say: select_cells, this will receive a message from the JavaScript telling it to server side select some cells for a user, which will then get broadcasted out to everyone else, and because we're monitoring the user cable, this is a field on that table, and so any changes to that will be broadcasted to everybody else. We'll need to of course, next create that select_cells method in our JavaScript. So active_users.coffee is going to be adding in the select_cells method here which will just delegate to the ActionCable @perform method, which calls the select_cells method and the server side channel. This select_cells string matches out with select_cells here, which will then receive the json hash that you give it. This is basically saying: Call this method with these arguments, and server side it, pulls those together, and then calls the appropriate method on the channel, which will then go update the user inside of the selected cells. That should be functional, and then, it looks like what we'll do next is basically be updating our selected cells and we'll set up some callbacks so that when there's a selection or deselection event, we will go update the selected cells on the server side and set them in the database, and then stream that over to the users, and then we'll go through and change our render method in order to grab all the cells that each user has and then draw them appropriately. Let's take this code and go make those changes.

app/assets/javascripts/spreadsheets.coffee

App.spreadsheet =
    active_users: {}

    new_user: (user) ->
        @active_users[user.id] = user
        @render_active_users()

    remove_user: (user) ->
        delete @active_users[user.id]
        @render_active_users()

    render_active_users: () ->
        $('#active_users_list').html(
            ("<li class=\"user-#{user.num}\">#{user.id}</li>" for id,user of @active_users).join("")
        )


        @selected_cells = []
        for id, user of @active_users
            if id != @current_user.id && (cells = user.selected_cells)
                @selected_cells.push(cells)
                cell = @hot.getCell(cells.r, cells.c)
                cell.classList.add('user-' + user.num)

    setup: () ->
        @selected_cells = []
        @cell_lock_callback = {}
        container = document.getElementById('spreadsheet')
        @hot = new Handsontable(container,
            minSpareCols: 1
            minSpareRows: 1
            rowHeaders: true
            colHeaders: true
            contextMenu: true
            afterSelection: () => @select_cells(arguments)
            afterDeselect: () => @deselect_cells()
            afterChange: (changes, source) =>
                if source != 'remote' && changes
                    for change in changes
                        App.spread_sheet_cells.set_cell_value(
                            { r: change[0], c: change[1] },
                            change[3]
                        )
            afterRenderer: () => @render_selected_cells()
        )

    render_selected_cells: () ->
        for cells in @selected_cells
            cell = @hot.getCell(cells.r, cells.c)
            if cell.classList.contains("current")
                cell.classList = "current"
            else
                cell.classList = ""

    select_cells: (cells) ->
        App.active_users.select_cells(r: cells[0], c: cells[1], r2: cells[2], c2: cells[3])

    deselect_cells: () -> App.active_users.select_cells(null)

    update_cell: (update) ->
        location = r: update.location[0], c: update.location[1]
        value = update.value
        @hot.setDataAtCell(location.r, location.c, value, 'remote')

        if update.lock == @current_user.id
            @cell_lock_callback[location]?()
            delete @cell_lock_callback[location]

MultiEditorPatch.inject()

$ -> App.spreadsheet.setup()

apps/assets/stylesheets/spreadsheets.scss

@mixin colored-border($color) {
    box-shadow:inset 0px 0px 0px 2px $color;
}
.user-1 { @include colored-border(#33a02c);}
.user-2 { @include colored-border(#e31a1c);}
.user-3 { @include colored-border(#ff7f00);}
.user-4 { @include colored-border(#6a3d9a);}
.user-5 { @include colored-border(#b15928);}
.user-6 { @include colored-border(#a6cee3);}
.user-7 { @include colored-border(#b2df8a);}
.user-8 { @include colored-border(#fb9a99);}
.user-9 { @include colored-border(#fdbf6f);}
.user-10 { @include colored-border(#cab2d6);}
.user-11 { @include colored-border(#ffff99);}
.user-12 { @include colored-border(#1f78b4);}

This is just simply defining some CSS colors for all the various users, so we'll only really support up to 12 users having unique colors, but this will do exactly what we want, so we'll just change the border color for those.

Let's try this out and see what happens. We have, it looks to be no JavaScript errors, but I don't see a user in the database, so that's potentially a sign that things are not working as we would want. Here you don't see the other users, but there we see that they eventually showed up, and they will eventually show up here, we'll see. Something is currently not working correctly. There it goes, but it's not working totally right as you can see that we don't see the appropriate stuff here. Maybe this is not rendering, maybe this is supposed to be an override of the render active_users.

It looks like all the code is not in the tutorial itself, but it is in the GitHub repo, so you can go make our changes accordingly for that, and there's a bunch of other things that we've noticed are just not included in that, which are things such as data.current_user and so on. We're going to grab this. See if there's a current user, and that goes into active_users.coffee as another else case here, it looks like we have a number of users here that we keep track of. We set the user's number as well so that would match up with the CSS that we wrote so that it correctly displays the right CSS color for each user. Then all of this code here is pretty much the same, but we set selected_cells at the beginning here so that when you call render, that will work appropriately. Then, when there's a new user it will render the selected cells. There's a bunch of little things like that that were not included in the tutorials, so we couldn't make it further past that, but we can just grab the coffeescript here and just take a looks at those changes. There's quite a bit of stuff that has changed, so we'll go into our spreadsheets.coffee, we'll replace that with our new one. Let's take a look at what we've got now. We have set_current_user, so you need to be able to keep track of who you are, so you need to do that. We can add in new users, and when you do a new user, you can also print out the number of users, which is nice. We'll do that and keep track of the number of users, we'll render the active_users list, but we'll also render out the selected cells. On that spreadsheet, we can go through and say: This user selected this, that user selected that, and so on. We needed somewhere to call that from, and that's why we're going to do that. These are pretty much the same as in the tutorial, and that should be that. There is the selected_cells there that we called, and there's probably a few more other changes that we need to include as well. We have the CSS, we got that. We need to also do this transmit current_user: @user so I'm not familiar with that, active_users_channel.rb needs to have that line in here, so I'm curious about this. Let's refresh our application, and we're seeing that we're getting the borders going around those users, which is good, and we can also go into our network tab here and let's refresh the page and pay attention to our ActionCable frames and see this current user that comes across. This just comes across as a message and tells it that the current user has it's id, and that's it. This transmit is basically doing a broadcast or sending it directly back to that user, so maybe it's not a broadcast sending it over to everyone, and it probably is not, and the transmit may actually be a one to one connection from the server to the browser and sending only that information back so that everybody doesn't think they're that current user, which is pretty cool. I don't think I knew that as a thing, but of course, it makes sense. Here you can see that the one without a border ends with Uv, and that's what we received as the current user, so that it knows which one is the current user and it can go and assign the proper user colors to everybody else except for you. That's pretty cool, and you can see here that the Uv one is brown, and we are Ub and that is how they separate those out. The transmit function is able to send data directly back, which is cool. I was not familiar with that, so we learned something there today about ActionCable, and we also have that select_cells method, but this is different than what was in the example. And also they corrected the field name on the user model, so there's that. As you might have expected, that was incorrect, so let's change this. This looks to be pretty much similar to the same thing that we had in the example of the update call, this is just setting the attribute and calling save which is the same as an update, but it deletes the action, so it looks like the formatting of the message changed a little bit. If we go into our spreadsheets.coffee. It appears that when we send that over, it just sends over the cells directly, and it's not going to be in that selected cells nesting that the original code showed. If we do this, and we open up localhost:3000, you can see that it hasn't cleaned up properly all of the old records when I've closed some of these tabs, so that's not great, but it is sort of functioning. We can see that some users are showing up selected as A1, which is neat, but I'm not able to see anyone else select these other cells and show up on any of the other displays. Maybe it's not totally working correctly, but we're making some progress, because they are at least showing something. We'll have to take a look at this a little bit more depth and see what we've got missing.

I just cleared out the database, and I went through the files again just to make sure that we had everything, and it looks like we might be in a better situation now. If you open up two tabs, you can see that it highlights the opposite person in green, and that's because the user number one is green. You're technically like user number zero or something, so you're the first person and you get no border. You're user undefined is what you are. You will show up as user undefined so you will show up with no CSS class in order to match that. If I go and select a B2, in the second tab, we can go back to the first tab, and we do see that B2 is selected, so if we select four cells, we still only see that there's that one cell selected, and that's probably fine, because this is technically, you're still only selecting B2 and you're just kind of expanded around. So it is showing up so that you can move around, and it does move around in the other browser tab, and everything is now caught up to where it needs to be. The tutorial just needed a little bit of work to keep up with all the changes, and to be fair this is starting to get a lot more complicated now that we actually have to sink data around such as the selected cells across our users. We can open up our ActionCable network and go into that, and see the frames. We can go and see one of these, you will see that when you click on one of these, you are sending out to everybody the change that you are now selecting cells, and there's an old value as well, so you can see what has changed. This could be a better way to see that. You can see that old value that did not have selected cells, and now you have your selected cells. It's not properly deleting that action out, but it is actually saving this, so that's column, and column two, and row and row two. Possibly this is supposed to be handling this squares maybe, maybe not. It looks like it is, so it can tell that I grab in this one column zero, column four, but on the other display, it doesn't actually do anything with those other ones. Let's refresh here and this should receive that frame, and we can see that zero four, but as you can see we only get that box around that individual one, so whatever the case for that, the example in the pictures didn't really show anymore than that, so I don't know if it does, but it is keeping track of the data. If you selected a larger than one cell, in theory you should be able to fix that, that is needed to be done. I'm not going to do that in this series because it's already long enough. This is a long tutorial to do on video and explain everything as we go

Transcript written by Miguel

Discussion