I am trying to use yrb-actioncable on Rails with yjs on Angular to have a collaborative text editor, I've never used ActionCable, Angular, Y.js, or CodeMirror before so I faced a lot of errors trying to make it work, I finally thought I had it when I stopped seeing any errors, but actually it still doesn't work.
I can see the editor on the screen and I can use collaborative editing between tabs of the same browser, I can see the requests to and from actioncable, but nothing else.
This is the whole repository, it's kind of a mess but I don't really know Angular and it's just a proof of concept to try and make things work. The DocumentChannel is the ActionCable part, the code-editor is the Angular component part. I'll paste them below too removing the commented and unused parts
require 'y-rb'
module ApplicationCable
class DocumentChannel < ApplicationCable::Channel
include Y::Actioncable::Sync
def initialize(connection, identifier, params = nil)
super
load { |id| load_doc 1 }
end
def subscribed
sync_from("document-1")
end
def receive(data)
sync_to("document-1", data)
end
def unsubscribed
end
private
def load_doc(id)
doc_content = Document.first.content
ydoc = Y::Doc.new
ytext = ydoc.get_text('mine')
ytext << doc_content
data = []
data = ydoc.diff unless doc_content.nil?
data
end
end
end
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'
import { OnInit } from '@angular/core';
import * as Y from "yjs";
import { WebsocketProvider } from "@y-rb/actioncable";
import CodeMirror from "codemirror";
import { CodeMirrorBinding } from 'y-codemirror'
import ActionCable from 'actioncable'
@Component({
selector: 'code-editor',
templateUrl: './code-editor.component.html',
styleUrls: ['./code-editor.component.scss']
})
export class CodeEditorComponent implements OnInit {
constructor(private http: HttpClient) {
}
ngOnInit() {
const accessToken = localStorage.getItem('accessToken')
const uid = localStorage.getItem('uid')
const client = localStorage.getItem('client')
const yDocument = new Y.Doc();
const consumer = ActionCable.createConsumer(`ws://localhost:3000/cable?uid=${uid}&access-token=${accessToken}&client=${client}`);
const provider = new WebsocketProvider(
yDocument,
consumer,
"ApplicationCable::DocumentChannel",
{}
);
const yText = yDocument.getText('codemirror')
const yUndoManager = new Y.UndoManager(yText)
const editorContainer = document.createElement('div')
editorContainer.setAttribute('id', 'editor')
document.body.insertBefore(editorContainer, null)
const editor = CodeMirror(editorContainer, {
mode: 'javascript',
lineNumbers: true,
})
const binding = new CodeMirrorBinding(yText, editor, provider.awareness, { yUndoManager })
// @ts-ignore
//window.example = { provider, ydoc, yText, binding, Y }
}
}
I can't seem to find what's wrong with it as I don't get any actual error, nor have I been able to get any more help from online guides nor the official docs that I've tried following. Could anyone guide me on this?
So, I managed to make it work, my frontend code didn't change much, but my backend code did, you can see the end result in this link, but I'll paste the channel here so you can see the differences.
# This method initializes the channel when someone tries to subscribe to it
# and there's no one here yet
# I imagine we should check here wether the document belongs to the user or not
def initialize(connection, identifier, params = nil)
super
# This is a method from Y::Actioncable
# it should return the full_diff of this channel's document
load { load_doc 1 }
end
# This method is called when someone subscribes to this channel
# If #initialize is called, this method will be called after
# that one
def subscribed
# This method is from Y::Actioncable
# It's just to facilitate set up, you should pass the actual
# document you want to sync (here we are always syncing the first document).
# The block is to save the document after syncing
sync_for(Document.first) do |id, update|
# Save it on the database
save_doc(id, update)
end
end
# Each time a change happens on the web this method will be
# called with the new data to update our document
def receive(data)
# With this we spread the new data to all subscribers
# so everyone is up to date with the document
sync_to(Document.first, data)
# Update the doc on the database, it would be good to not do
# this every time this method is called, or maybe delay it
# so it doesn't block every request
save_doc(id, update)
end
# This method is called when a subscriber stops using the document
def unsubscribed
end
private
def load_doc(id)
# For this POC we are using only the one document
# otherwise we should look for the document with the given id
doc_content = JSON.parse(Document.first.binary_doc)
# Return the document binary
doc_content
end
def save_doc(id, update)
# Since we are only using the one document on this POC
# We update that document only, otherwise we should look
# for the correct document to update
Document.first.update(binary_doc: update)
end
Now I'm glad to say it works just fine ^^