Add Code Editing To Your Weweb App with Monaco!

Ray Deck shows how to embed Monaco, an open-source code editor, into a WeWeb app using a CDN and custom JavaScript. He walks through importing Monaco's CSS and scripts, handling WeWeb's iframe constraints with getFrontWindow, and syncing editor changes back to WeWeb variables via workflows.
Chapters
Hey, so WeWeb is a wonderful low-code platform, but it sometimes involves bringing some code to it. In fact, you're going to find that sometimes you want to create apps that you want to add some code in them. And editing code like in ordinary text boxes can be quite the drag because it doesn't give you the kind of experience that you sort of expect as a code editor. You're going to find there's actually a fairly standard experience that you associate with editing code that you find in Visual Studio Code, Cursor, WinSurf, and JSFiddle, all sorts of stuff. And that's because they all use the same open-source package from Microsoft called Monaco, which is an editor that is, like I say, open source that you can use to get that same experience over and over and over and over. And it allows you to bring this experience that you associate with an IDE into your web browser and into your web apps. Now, ordinarily, Monaco uses things like service workers and what have you that can be a little tricky to integrate into a web application. And this is one of the reasons why people use things like, you know, npm or what have you to integrate it into the source code itself. But when we're using a low-code system, using WeWeb in particular, you know, our ability to bring in external code is a bit more finite, right? So there are custom codes that we can do through the Vue.js integration with WeWeb, and that can be a discussion for another time. But often what we want to do is bring something in that allows us to stay within the WeWeb and the low-code environment without having to go to the outside world. So how do we do that? We do that by connecting with what we call content delivery networks that make a lot of really interesting external code available, and then pull it into our app a bit more on demand through what are called script tags. And in WeWeb, because it's not strictly just a web page, but actually a web application that's running Vue.js and what have you, we gotta jump through a couple more hoops, but they're not so bad, and we can actually make it all work work inside the low-code environment and then control it through the low-code environment. Let's talk about how. The, uh, what we can do here is, uh, you know, first go searching on the web for how do we connect to, uh, Monaco, particularly if we want to do it through a CDN. So whenever you're looking into, you know, the, these, uh, you know, low-code systems in general, the, the keyword you're looking for isn't just Node. You want to do it from what's called a CDN. And even if you're going to be getting it from a CDN that distributing, you know, the CDN version of a Node package, that's going to be like the mental model you want to bring to this in order to be able to get more success out of it. And fortunately, this site called Stack Overflow, which is one of the most popular sites among developers on the web, generally very codey, but often can give you answers and shortcuts very quickly as to how to do this. And here you have a wonderful answer here that's talking about how to make all this stuff work. And there are showing how to have just an arbitrary piece of HTML. You don't have to have any Node.js or anything else. With it, they were able to make Monaco work. Let's study this for a minute to see how they made it work, and then how we can bring that into our WeWeb environment. The first is we ask, what are they bringing in from the outside? There are actually two things they're bringing in from the outside. The first is the CSS, which is styling the editor. And that's actually a really important component because editor is very complicated. And we usually get styles through what's called a link tag to bring a stylesheet in. And there can be multiple stylesheets you can bring in, but we're just going to use this default one as a starting point. The second thing is we have what's called a script tag where we bring in outside code. Now the weird thing about Monaco is all we're going to load in from the outside first is going to be their loader. With actually a relatively small amount of JavaScript. After that, it actually runs some code, some actual JavaScript on the page. That's what these lines are. And it's going to first configure the loader, because the require is actually the instance of the loader that was brought into that script tag, to say, hey, please know this is going to be the editor I'm going to want, basically the Visual Studio version. And then I'm going to— connect it to an element on my page using getElementById, right? I'm gonna find an element on my page with a correct identifier, and we will then initialize it with some code. So we're gonna talk about how to get this in, we're gonna talk about how to get the initial code in, and then we'll talk about how to make it editable. But in order for us to make good use of this, we're going to need to bring some code into your, um, you know, how to bring that external stuff into your WeWeb. Now, WeWeb does have a feature called custom code, and custom code allows you to bring stuff into the header, uh, kind of like the script tags we were just looking at, um, but it only works for deployed sites, right? So that means we're not going to be able to test things or test interactivity when we're inside our editor. And so that's the reason why StateChange has made this embed converter that allows you to take these various script tags and bring them into a format that, that WeWeb can just consume through its workflows. So if I sort of take this first link tag here and I paste it into here, bing, and I have a link tag all the way down to the bottom, great. I can say, you know, convert it to a JavaScript action. Then I can copy that to the clipboard, go back over to my WeWeb to my just blank project I got here. And what I will usually do in these circumstances is do an app workflow. And I'm gonna say new here, and say onAppLoad, probably before fetch any collections. I'm gonna add an action, and I'm going to have that action be the custom JavaScript action. And I'm going to, I'm just gonna expand this for a second to make it easier to read, and I paste in what I just have from the embed. That's all there is to it. Now we have that stylesheet, which will be loaded onto the page. The next thing we talked about is wanting to get that loader, right? The second script tag. Fantastic. I will get that script tag. Now where this was on the page doesn't matter. We just want to make sure the code gets loaded. So we're going to go copy this, right? The script tag. We're going to convert it. Copy, close clipboard, go over here. And actually I can add it to the same JavaScript step. There's a little button over here that says format document. I totally recommend using it. You click this and now you have this sort of snaps stuff back into place. In addition to looking a little bit nicer and making it a little bit easier to read, it also gives you more confidence that in fact this doesn't actually have some sort of syntactic problem with it. If it didn't snap in, it would be an indication that there is some sort of problem that we need to examine more carefully. Okay, so this will load at the beginning of the page. And if I test it, it'll run, but it's not very interesting. So now let's close this up for a second. That's happening at the very beginning of the application to make sure that the code that we need is going to be loaded. But the next thing we need is to, well, actually have an editor, right? That's the whole purpose of this exercise. So for that, we're going to need a rectangle. A rectangle is a div. Let's just bring it out. We will say layout, blank, flex, and it calls it a flexbox, organizes as flexbox. That's fine, we don't really care. Here, I'm just going to set the width to 100%. And set the height to a fixed 500 pixels, just as a starting point here. So now I've got 500 pixels, I've got my div, and you know what else? I'm just gonna make sure that it's easy for us to see where it is. I'm gonna make it nice and green. Hello, Mr. Green Guy. No, I didn't want that guy to be green. Ooh, let's cancel that. It's the flexbox I want to be green, right? And the flexbox is going to be, Yep, green and green, and wow, that's a little hideous. Let's see what we can do to get rid of that. Um, the— we're gonna— so, but, but in order for us to load it up, right, we had the initial thing we load up with the application. We don't actually want to bring the code editor into play until we actually have a place to put the code editor, which is this rectangle. So the way we synchronize those two events is going to be through not click events, but through what we call mounted. This is part of what we call the lifecycle of the of the div, like sometimes it gets— Vue.js thinks about it, it mounts onto the page, and then removes it from the page depending on where we are in the course of the application. So basically, when it mounts, that's going to be when we want to do some code. And we're going to have some custom JavaScript here. So here we have Edit Code JavaScript, and we open this up. So let's find out what JavaScript we need to run. And here, what I will do usually is I will, you know, copy all this stuff and then just paste and like paste it over in there and work with it on the other end. And that, but we're gonna need to examine it for a second. Okay. So there are a couple of things going on here. The first is this idea of require and Monaco and document. You might wonder where are all those three things coming from? And you know what? WeWeb's gonna wonder the exact same thing because there's an assumption built into the fact that it was running just on the HTML. That require was attached to the window, and document is attached to the window, which are— this is the global context in JavaScript. But that's not the case in the WeWeb editor, because WeWeb editor is actually this big web page with an iframe that contains your actual document in it. So you want to make sure you're referencing the part that's the document that has the actual application, as opposed to the window that has the editor. Because the window has the editor, you don't want to have all this stuff, you don't go messing with the editor, you want to mess with just with your application. Now, when the document— when your application publishes, there's actually not a difference between these two things. Because your app is just sort of running. It's in the main window. And we're all glad for that. But it does mean that we want to maintain those distinctions so that we can have an easier editing experience in order to be able to modify this stuff. So to do that, we're going to need to make sure that we are explicitly referencing the correct context, which is to say the correct correct window. To do that, we use dub dub lib, which is the gateway to all wonderful things that WeWeb provides into JavaScript land. And we're going to do getFrontWindow.require.config. So now instead of getting it from, uh, from, uh, front context, from the context of the global, uh, which is what the HTML assumed, we're going to be a little more sophisticated about it, and we're going to say it's coming from the front window context, which is just inside that application. If you just remember to, instead of using global context, throw in www.lib.getFrontWindow, you can solve a lot of problems for yourself. And here are 3 examples that all work. And you can even do that for the same one, the document here. I'll also tell you there's a shorter version of getFrontWindow.document, which is called getFrontDocument. They both work exactly the same way. And now we have getElementById here. To a container. And what getElementById means is it's looking for something on the page that has an ID, which is a specific attribute that you can control in WeWeb, called container. You know what? We're gonna be a little fancy about this. We're gonna call it monaco-demo. And then we're going to be happy with this, okay? So I'm looking for an ID that's called monaco-demo. And if I run this, it doesn't actually do anything. In fact, I look over here, it says TypeError: can't read properties of undefined. And reading config. Why? Because require hasn't run yet. Why hasn't require run yet? Require hasn't run yet because that first onApplicationApp workflow apparently has not run yet. That's okay. That's okay. So what we're going to do here is actually set the stage for just one more thing, which is we're going to say that this big rectangle here This is the one we want to set the ID to. And you can set the ID using the fine-tuning, which is the little, you know, like little control board sliders. And we call this Monaco demo. Cool. Now we made a bunch of changes. And I'll tell you that whenever I'm working with like this external CDN stuff and like working with app load and whatever, it can be a lot faster to find out what things are going to work just by pressing the refresh button on the whole web page. Not the fastest experience in the world, but it goes. Hey, look, we got ourselves an editor. You'll see that the editor has already pushed itself down by a little bit. We're not totally sure why, but you can play with layout considerations afterwards. But here we have a function and a console.log. Whoa, where'd it go? Everything seems to be there. Let's just run it again. Oh, and it didn't work. Well, that's interesting. Let's find out why it didn't work. Uh, let's take a look at the log, and it says can't find, uh, properties of undefined, right? Basically the same problem, can't read config. And config, if you remember— and I appreciate there's a lot to remember in here— um, uh, it was saying that because this config thing came from something called require. Now, this is usually a timing problem. So even though we said, please load my code at the very, very beginning of the lifecycle of the application, it's actually quite likely that it did not load it at the very beginning of the lifecycle of the application. And instead it is, or actually it ran early, but then it ran this so fast after that require hadn't had a chance to load in before this code was running. So we want to make sure that A runs, then B runs. Now A and B are running in completely different workflows. How do we make them wait for each other? And the way we do that is by telling it to please wait for that. So we make a quick little function here, which will have A return new Promise, r, and this is— I'll call it resolve instead. And we'll set timeout of— so what this will do is cause it to sleep. Right? What I'm gonna say is while, and I'm gonna say while do do do do do do do, if the required doesn't exist yet, right? So the way I test for that, I'm gonna say null, await sleep 500. So what this is going to do is gonna say, hey, is it here? Nope, sleep for 500 seconds. Is it here? Nope, wait, no, we'll sleep for 500 milliseconds. And we'll say console.log counter 1. This is what we call a kill switch, because whenever you have a while, you're just concerned that you're going to go too many times. So say if counter is greater than 100, break. Basically, it's just to make sure that we don't run it too many times. Log, this ran too many times. So these are just little guardians we put into place to make sure we don't get into infinite loop situations, which have definitely happened before. And the idea is that we are saying, as long as require is null, sleep, and then try it again, and then try it again, and then try it again. And frankly, if it runs for 100 times, which in this case would be about 50 seconds if it's all waiting the proper way, then it will, uh, then it will abort. And if for some reason it's not awaiting, it'll go 100 times very fast and then abort. But the goal here is that we're not going to run any of the rest of this until we know or feel confident that require is ready for us to be able to do the rest of does the work. And then that way we don't have that race condition that we're saying basically we have A and then B, which might start before A finishes. It's waiting to know that A is finished before it continues. All right, let's see how, uh, let's see how that works for us. Uh, we're going to close this up. We're going to run this JavaScript, test it. Uh-huh, it says it succeeded. Close. Yep, so you see some stuff. But now my big question is, does it Run. Runs. Runs. Now there is a— I will often, when I'm working in anything in, in WeWeb, say please, you know, look at my dev tools at the same time, because that— because whenever you're building a web app, it's of course a web app, that means it's running inside the browser. So we have this and we'll be able to see what's going on. You can see this thing that I put into the console.log actually showing up. So basically the first time it runs, it wasn't quite there. And so wait a half second, it was there, it continued. So it was doing its job. That sleeping, that waiting until the next thing shows up, super, super valuable technique. And now we have our editor that's running. And the editor says it's got this stuff in it, and that's great. Now I would like to be able to bind this and be able to have this get reflected in a variable so that we can actually be working with the data that's inside this header. And that involves two parts. The first is being able to initialize it with data that is from something else, and the second is being able to edit— being able to respond to changes in the editor. And to do that, we're going to Well, there are a couple things we're going to need to do here. One thing we will need to do is we're going to need to— well, let's just start by associating it with a variable, right? And we'll initialize it with a variable. To do that, we'll go over data and we're going to say we'll create a new variable. And the variable, we'll call it Monaco content. And the content is going to be, hello there. Actually, you know what? We'll say, you know, function hello message. You know, and we'll say, uh, message. Okay, that's a very annoying experience, but it will give you an alert every time that actually runs. We're not actually gonna run this function, but it's just gonna be code that we can have available to us. This is all fine. And that's gonna be— and I don't really want for the description, I want it for the default value, right? So we're just gonna start that way. Hey, look, it sort of starts. You'll notice that here I've got enters, when here it doesn't actually show the enters, and that's just a limitation of the way WeWeb is reflecting this data back to you. So now we've got Monaco content. Let's click into here for a second. And, and we'll say inside my flexbox, I've got my custom JavaScript. Now remember previously we were like pasting in this value between these two backtick characters? Well, instead we could click on the X here, and we could say click in Monaco content, and it will pull it from a variable instead. Let's just show how that works. Close, and then again, just do refresh. See? So now we've got the correct stuff. The next thing we want to do though is going to be, well, if I make a change in here, I want to be able to reflect that change in a variable that I can then do work with. And the way we do that in WeWeb is through creating workflows. So we want to have a workflow that's going to respond to this thing changing. So let's on spec create ourselves a workflow. We'll create a new workflow that on execution we'll call it update Monaco content. It's going to take a parameter called content, uh, called content. And we'll add an action here and the action is going to be to change a variable value and the variable value is The variable we're going to change, content, we're going to change it to what came in through the parameters of the action. Look at that. Okay, so we're just gonna— so all it does is just change the, the variable, uh, and we will keep track of it by making a little text element underneath this, right? So we'll say underneath blank, uh, we're going to say add another, um div underneath that. And this one is— which one's which? This is the Monaco demo one. Okay, so let's make sure that Monaco goes on top here. Uh-huh. And then this guy here, we're just going to add a text element. Put that inside. And the flexbox, we're just gonna give it a little bit of a buffer, 'cause this thing seems to be dragging underneath. And the way we're going to do that is by saying, we're gonna say margin on the top is 50 pixels. And then the text itself, yeah, just contains some text. But the text it's gonna contain, as I go back over to here, is going to have, where's text come from? Text is going to be whatever's in the content, okay? All right, so, so far so good. Everybody, hello everybody is all here, we're happy. I'm just gonna make this guy a little bit bigger to make it easier for us to look at. And by doing that, we'll say the width will be 100%, and the height I'll just allow to grow as it sees fit. Okay, so that now the trick though is that right now I can do an edit, there, there, things, and nothing changes, right? So the— what we want to do is make it change. So for that, we're going to need to plant, um, a listener that's inside the Monaco editor, right? So it's not going to be strictly a workflow that is, you know, coming from WeWeb. It's going to be a workflow that we're going to need to plant inside the JavaScript when we build out this you know, this element here. And for that, we're going to go into the entire workflow, go into the JavaScript. And the, the gag here is that having done this, you know, require, right, um, or rather, after we've done the require, we want to do a create, right? We're creating this editor, but then we lose track of the editor. So what I'm going to propose is that we make the editor into a variable that your browser is going to remember. So I'll say dub dub lib dot get front window dot, uh, Monaco editor equals. Okay, so now we're going to have a variable on the browser called Monaco editor. This is not the same thing as being a WeWeb variable, but you know, WeWeb can access it by itself looking for dub dub lib get front window Monaco editor. That can become valuable later. Um, and then down here I can say www.lib.get_front_window.monaco_editor.get— and, and I'm pretty sure that at this point what I want to do is run get_model. Because the model is the thing that's inside the Monaco editor that actually deals with the relationship with the text. And then I'm going to have— and then I want to have a— look at an event called onChangeContent. And this is going to be— this will just require a function that runs whenever we change the content. And for this, we now want to run our workflow, uh, with— and, and tell it to please update, update the content based on the content that we have inside the model. Now the way we get the model is const content equals www.lib.getfrontend.monacoeditor.getmodel.getvalue. There's a lot to it, isn't there? Um, but like it's sort of all strings across. And by the way, if you're trying to figure out like how to do some of this stuff, uh, there is, uh, there's actually wonderful, uh, sorry, that's the wrong thing. Send, uh, copy and paste. Let me say, make sure that works. There we go, right? So this is the documentation for the Monaco editor and tells you about how all the stuff works. It's very dense, it's very codey, but at least gives you names for things you can start to work with. And, you know, throwing this as a context into ChatGPT or Claude is also going to help you along. So the, um, and like, this is all from the same website, obviously, just as this is more from the docs. The, uh, so back over to here, we, we're going to say that the, uh, the value of the content, right, is just the, the value from the model. And what we want to do is please run that workflow. Now how do we do that? How do we run a workflow from inside the JavaScript land? We do that through www.lib.executeWorkflow. executeWorkflow takes two arguments. The first argument is If I go to actions here, where it says update Monaco content, the first argument is going to be the ID of the workflow. Right, so now I pass that in, I put between these quotation marks, and it turns red, you know you did it right. Now the second argument is going to be an object, and the object is gonna be of the parameters. And the question is what the content is. And so the key that we'd set up in the other thing was called content, and the value is the content variable that we just created here. So I'm just gonna say, how about I just say Monaco content, and I'll say Monaco content, just so you have an easier distinction between what the key is and what the value is. As always, again, a little more complicated, I will just press this button to make sure that like things snap into place. It becomes a little more vertical, a little bit more readable, and finally, it just gives me that confirmation, right, that I'm doing it more or less correctly. Okay, so now it looks like I've got execute workflows, seems to be flying, that's great. Let's close this up, and we're going to test the, you know, we can test, I suppose, this JavaScript, although the question is, did it mount? Oh, look at that, get front widnow. That's because apparently I'm incapable of spelling. So let's find out why I did that mistake. Ding ding ding ding ding! Get front window. Get front— oh, look at that, window. Coding is a precise endeavor. And then if I run that again— yeah, yeah, yeah, yeah, yeah. Okay, if I clear that, run that— uh-huh, already has context attribute, blah blah blah. Okay, you'll notice that I've been creating a whole lot of these headers, and let's get out of this business. Obviously the test We only mount once, and that would be the more proper behavior. Okay, all right, all right. So now we have all this. Look at that, see, it changes right down here. And the reason it's changing down here is because this text element is bound, right, to the— if I open this up, the text n is actually bound to the variable. And whenever I'm making a change in the code up here, it's changing and it's running the workflow with the edit. There's more that you can do here. You can make it so that making change here goes and edits stuff inside your Monaco. You can add all sorts of additional complexities to like the, uh, you know, plugins, etc., that you work with in Monaco. And I'll tell you that like once you start getting beyond, uh, you know, beyond a certain level of complexity, you're going to want to have more control over this. Using, you know, using tools that are like a little bit native to code and then bringing that in to be working inside your WeWeb. This is where custom components, etc., can become helpful. But man, there's so much you can accomplish bringing a relatively straightforward code editor to the benefit of your users. And hopefully this has shown how you can do so in WeWeb in a relatively short period of time. Certainly bringing code into a no-code environment definitely counts as being potentially hard as 5% for a lot of things. So if you got more questions about that, you know, drop, drop a comment down below. And if you want to learn more about doing these things more in general, please come and check out StateChange where we work on these kinds of hard questions routinely and have a very good time doing it. I will look forward to seeing you next time.










