<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Houman Codes]]></title><description><![CDATA[Houman Codes]]></description><link>https://houman.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 11 Mar 2026 22:07:31 GMT</lastBuildDate><atom:link href="https://houman.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><atom:link rel="first" href="https://houman.hashnode.dev/rss.xml"/><item><title><![CDATA[How to Create a Multiplayer Web App in OutSystems using Liveblocks]]></title><description><![CDATA[<h2 id="heading-the-what-is-this-about-part">The What is this about? part</h2>
<p>Collaborative web applications are increasingly important because they enable real-time teamwork and communication, which is crucial in todays remote and hybrid work environments. As businesses seek more efficient ways to work together from different locations or engage with customers, we need an easy way to integrate real-time collaborative experiences into our business apps.</p>
<p>This article gives a tutorial on how to get started building your own collaborative multiplayer experience in <a target="_blank" href="https://www.outsystems.com/"><strong>OutSystems</strong></a> using <a target="_blank" href="https://liveblocks.io/"><strong>Liveblocks</strong></a>.<br />The goal of this simply to demonstrate how to integrate Liveblocks into OutSystems with Miro or Figma like live cursors to see the presence of the other users in the same <em>Room</em> as seen on the picture above.</p>
<blockquote>
<p><em>OutSystems</em> is a low-code platform that helps you build apps quickly with a visual interface and ready-made components. You can also add your own custom code using .<em>NET</em> and <em>JavaScript</em> for more complex features. Its a great tool for developers who want the ease of low-code with the flexibility of high-code.</p>
<p><em>Liveblocks</em> is a platform that makes it super easy to add real-time collaboration features to your apps. It lets you build things like multiplayer editing or live presence, with just a few lines of code. Plus, if you want to get fancy, you can extend it with your own custom JavaScript to create unique, interactive experiences. Its perfect for businesses looking to add live, interactive elements to their projects quickly.</p>
</blockquote>
<h2 id="heading-the-tutorial-part">The tutorial part..</h2>
<h3 id="heading-the-stuff-you-need-to-do-before-diving-into-the-tutorial">The stuff you need to do before diving into the tutorial..</h3>
<p>Before we get started we need to have a few things in place:</p>
<ul>
<li><p>IDE of choiceI use <a target="_blank" href="https://code.visualstudio.com/download">VS Code</a></p>
</li>
<li><p>Be able to run <a target="_blank" href="https://nodejs.org/en">NPM</a> (run npm -v in terminal to check version installed)</p>
</li>
<li><p>OutSystems Environment and Service Studio (<a target="_blank" href="https://www.outsystems.com/Platform/Signup">Sign up</a> for free personal environment)</p>
</li>
<li><p>Liveblocks account (<a target="_blank" href="https://liveblocks.io/signup">Register for a free</a>)</p>
</li>
<li><p>Basic knowledge of OutSystems and JavaScript/TypeScript</p>
</li>
</ul>
<h3 id="heading-the-npm-part">The NPM part</h3>
<p>Start by <em>creating a folder</em> for your code and open the folder in VS Code (<em>as admin</em>).</p>
<p>Then, we need to initialize the package.json file with default values</p>
<p><code>npm init -y</code></p>
<p>Now we are ready to install the latest version of Liveblocks Client and its dependencies using NPM install</p>
<p><code>npm install @liveblocks/client</code></p>
<p>There was a catch in using Node modules in OutSystems, as it does not support integrating Node modules directly, so we need to use some workarounds in order to make it work. Namely, we need to bundle the code into a single JavaScript file with the necessary dependencies included i the file. To do this we need to install a bundler. In this case, I am going to use ESBuild.</p>
<p><code>npm install esbuild</code></p>
<h3 id="heading-the-code-part">The code part</h3>
<p>Now we will implement the code. First off create a file called <strong><em>app.ts</em></strong> in the root of your folder.</p>
<p>We first need to import our <em>Liveblocks/clien</em>t module which will be used to create a client with a<em>Room</em> and <em>Authentication</em> to communicate with the Liveblocks server. We also need to declare a structure that will hold the properties of each user's presence in the <em>Room</em>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@liveblocks/client"</span>;

<span class="hljs-keyword">declare</span> <span class="hljs-built_in">global</span> {
  <span class="hljs-keyword">interface</span> Liveblocks {
    <span class="hljs-comment">// Each user's Presence</span>
    Presence: {
      cursor: {
        x: <span class="hljs-built_in">number</span>; <span class="hljs-comment">// The x-coordinate of the cursor's position.</span>
        y: <span class="hljs-built_in">number</span>; <span class="hljs-comment">// The y-coordinate of the cursor's position.</span>
        name: <span class="hljs-built_in">string</span>; <span class="hljs-comment">// The user's name.</span>
        city: <span class="hljs-built_in">string</span>; <span class="hljs-comment">// The user's city.</span>
        countryCode: <span class="hljs-built_in">string</span>; <span class="hljs-comment">// The user's country code.</span>
      } | <span class="hljs-literal">null</span>;
    };
  }
}
</code></pre>
<p>Let's define some other variables and constants that we need. The <em>PUBLIC_KEY</em> can be found in the Liveblocks Dashboard Projects  {Environement}  API keys.</p>
<blockquote>
<p>Be aware that for this demo we will be using a public key, but for production scenerios more robust authentication should be used. Learn more about them here: <a target="_blank" href="https://liveblocks.io/docs/authentication">https://liveblocks.io/docs/authentication</a>.</p>
</blockquote>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> PUBLIC_KEY = <span class="hljs-string">"{public_key}"</span>;
<span class="hljs-keyword">let</span> roomId = <span class="hljs-string">"LiveblocksOutSystemsDemo"</span>;
<span class="hljs-comment">// User's location name for cursor</span>
<span class="hljs-keyword">let</span> locationData: { city: <span class="hljs-built_in">string</span>; countryCode: <span class="hljs-built_in">string</span> } ;
<span class="hljs-comment">// User's cursor name</span>
<span class="hljs-keyword">let</span> cursorName: <span class="hljs-built_in">string</span>;
<span class="hljs-comment">// Container for cursors that will be implemented in OutSystems</span>
<span class="hljs-keyword">const</span> cursorsContainer = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"cursorscontainer"</span>)!;
<span class="hljs-comment">//Colors assigned to each user</span>
<span class="hljs-keyword">const</span> COLORS = [<span class="hljs-string">"#DC2626"</span>, <span class="hljs-string">"#D97706"</span>, <span class="hljs-string">"#059669"</span>, <span class="hljs-string">"#7C3AED"</span>, <span class="hljs-string">"#DB2777"</span>, <span class="hljs-string">"#3B82F6"</span>, <span class="hljs-string">"#16A34A"</span>, <span class="hljs-string">"#F59E0B"</span>, <span class="hljs-string">"#EC4899"</span>, <span class="hljs-string">"#4B5563"</span>];
</code></pre>
<p>Now let's initiate the client and assign the collaborative Room</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> client = createClient({
  throttle: <span class="hljs-number">16</span>, <span class="hljs-comment">// websocket throttle in milliseconds to smooth the realtime animations</span>
  publicApiKey: PUBLIC_KEY,
});

<span class="hljs-comment">// Enter room with initial presence</span>
<span class="hljs-keyword">const</span> { room, leave } = client.enterRoom(roomId, {
  initialPresence: { cursor: <span class="hljs-literal">null</span> },
});
</code></pre>
<p>To receive other users' presence we need to subscribe to events where cursors change. We will also add the functions for cursor CRUD operations later on.</p>
<pre><code class="lang-typescript">room.subscribe(<span class="hljs-string">"others"</span>, <span class="hljs-function">(<span class="hljs-params">others, event</span>) =&gt;</span> {
  <span class="hljs-keyword">switch</span> (event.type) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">"reset"</span>: {
      <span class="hljs-comment">// Clear all cursors</span>
      cursorsContainer.innerHTML = <span class="hljs-string">""</span>;
      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> user <span class="hljs-keyword">of</span> others) {
        updateCursor(user);
      }
      <span class="hljs-keyword">break</span>;
    }
    <span class="hljs-keyword">case</span> <span class="hljs-string">"leave"</span>: {
      deleteCursor(event.user);
      <span class="hljs-keyword">break</span>;
    }
    <span class="hljs-keyword">case</span> <span class="hljs-string">"enter"</span>:
    <span class="hljs-keyword">case</span> <span class="hljs-string">"update"</span>: {
      updateCursor(event.user);
      <span class="hljs-keyword">break</span>;
    }
  }
});
</code></pre>
<p>Now lets add some Listeners to update the presence on each Pointer move and leave. Notice that we also update the user's name from an input field which we will later on define in OutSystems.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Listen for pointer movements</span>
<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">"pointermove"</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-comment">// Get cursor container boundaries</span>
  <span class="hljs-keyword">const</span> rect = cursorsContainer.getBoundingClientRect();
  <span class="hljs-comment">// Calculate cursor x and y within the container bounds</span>
  <span class="hljs-keyword">const</span> x = <span class="hljs-built_in">Math</span>.min(<span class="hljs-built_in">Math</span>.max(event.clientX - rect.left, <span class="hljs-number">0</span>), rect.width);
  <span class="hljs-keyword">const</span> y = <span class="hljs-built_in">Math</span>.min(<span class="hljs-built_in">Math</span>.max(event.clientY - rect.top, <span class="hljs-number">0</span>), rect.height);

  <span class="hljs-comment">// Get the user's name from input field</span>
  <span class="hljs-keyword">const</span> inputName = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"nameInput"</span>)! <span class="hljs-keyword">as</span> HTMLInputElement;
  cursorName = inputName.value ? inputName.value : <span class="hljs-string">""</span>;

  <span class="hljs-comment">// Update user's presence with cursor position and info</span>
  room.updatePresence({
    cursor: { x: <span class="hljs-built_in">Math</span>.round(x), y: <span class="hljs-built_in">Math</span>.round(y), name: cursorName, city: locationData.city, countryCode: locationData.countryCode },
  });
});

<span class="hljs-comment">// Set cursor to null when pointer leaves the document</span>
<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">"pointerleave"</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  room.updatePresence({ cursor: <span class="hljs-literal">null</span> });
});
</code></pre>
<p>At the end of the code, we need to add some code to visualize the cursors with labels.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Updates the cursor's position and visibility based on user presence</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updateCursor</span>(<span class="hljs-params">user</span>) </span>{
  <span class="hljs-keyword">const</span> cursor = getCursorOrCreate(user.connectionId);

  <span class="hljs-keyword">if</span> (user.presence?.cursor) {
    <span class="hljs-comment">// Set cursor position and make it visible</span>
    cursor.style.transform = <span class="hljs-string">`translate(<span class="hljs-subst">${user.presence.cursor.x}</span>px, <span class="hljs-subst">${user.presence.cursor.y}</span>px)`</span>;
    cursor.style.opacity = <span class="hljs-string">"1"</span>;

    <span class="hljs-comment">// Update cursor label text and position</span>
    <span class="hljs-keyword">const</span> cursorText = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">`cursortext<span class="hljs-subst">${user.connectionId}</span>`</span>);
    <span class="hljs-keyword">if</span> (cursorText) {
      cursorText.style.transform = <span class="hljs-string">`translate(<span class="hljs-subst">${user.presence.cursor.x}</span>px, <span class="hljs-subst">${user.presence.cursor.y + <span class="hljs-number">20</span>}</span>px)`</span>;
      cursorText.textContent = user.presence.cursor.name ? <span class="hljs-string">`<span class="hljs-subst">${user.presence.cursor.name}</span>`</span> : <span class="hljs-string">`<span class="hljs-subst">${user.presence.cursor.city}</span>, <span class="hljs-subst">${user.presence.cursor.countryCode}</span>`</span>;
      cursorText.style.opacity = <span class="hljs-string">"1"</span>;
    }
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Hide cursor and label when user presence is not available</span>
    cursor.style.opacity = <span class="hljs-string">"0"</span>;
    <span class="hljs-keyword">const</span> cursorText = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">`cursortext<span class="hljs-subst">${user.connectionId}</span>`</span>);
    <span class="hljs-keyword">if</span> (cursorText) {
      cursorText.style.opacity = <span class="hljs-string">"0"</span>;
    }
  }
}

<span class="hljs-comment">// Retrieves or creates a cursor element for the specified connectionId</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCursorOrCreate</span>(<span class="hljs-params">connectionId</span>): <span class="hljs-title">HTMLElement</span> </span>{
  <span class="hljs-keyword">let</span> cursor: HTMLElement | <span class="hljs-literal">null</span> = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">`cursor<span class="hljs-subst">${connectionId}</span>`</span>);
  <span class="hljs-keyword">let</span> cursorText: HTMLElement | <span class="hljs-literal">null</span> = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">`cursortext<span class="hljs-subst">${connectionId}</span>`</span>);

  <span class="hljs-keyword">if</span> (cursor == <span class="hljs-literal">null</span>) {
    <span class="hljs-comment">// Clone the cursor template and assign a unique ID</span>
    cursor = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"cursortemplate"</span>)!.cloneNode(<span class="hljs-literal">true</span>) <span class="hljs-keyword">as</span> HTMLElement;
    cursor.id = <span class="hljs-string">`cursor<span class="hljs-subst">${connectionId}</span>`</span>;
    cursor.style.fill = COLORS[connectionId % COLORS.length];

    <span class="hljs-comment">// Create and style the cursor text element</span>
    cursorText = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
    cursorText.id = <span class="hljs-string">`cursortext<span class="hljs-subst">${connectionId}</span>`</span>;
    cursorText.style.backgroundColor = cursor.style.fill;
    cursorText.className = <span class="hljs-string">'cursortext'</span>;
    cursorText.style.position = <span class="hljs-string">'absolute'</span>;
    cursorText.style.opacity = <span class="hljs-string">'0'</span>; <span class="hljs-comment">// Initially hidden</span>

    <span class="hljs-comment">// Append the cursor and text elements to the container</span>
    cursorsContainer.appendChild(cursor);
    cursorsContainer.appendChild(cursorText);
  }

  <span class="hljs-keyword">return</span> cursor;
}

<span class="hljs-comment">// Removes the cursor and associated label for the given user</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">deleteCursor</span>(<span class="hljs-params">user</span>) </span>{
  <span class="hljs-keyword">const</span> cursor = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">`cursor<span class="hljs-subst">${user.connectionId}</span>`</span>);
  <span class="hljs-keyword">const</span> cursorText = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">`cursortext<span class="hljs-subst">${user.connectionId}</span>`</span>);
  <span class="hljs-keyword">if</span> (cursor) {
    cursor.parentNode!.removeChild(cursor);
  }
  <span class="hljs-keyword">if</span> (cursorText) {
    cursorText.parentNode!.removeChild(cursorText);
  }
}
</code></pre>
<p>As seen previously in the code we also have the user's location in the Presence structure. This is so that we can see a label before the user has input any username. So let's add this function at the end.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Function to fetch geolocation data based on IP address</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchLocation</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'https://ipapi.co/json/'</span>);
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();
    <span class="hljs-keyword">return</span> {
      city: data.city,
      countryCode: data.country_code
    };
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to fetch location data:'</span>, error);
    <span class="hljs-keyword">return</span> {
      city: <span class="hljs-string">'Unknown'</span>,
      countryCode: <span class="hljs-string">'Unknown'</span>
    };
  }
}
</code></pre>
<p>and we need to fetch location data once at the start of the code, for example, before creating the client.</p>
<pre><code class="lang-typescript">..
<span class="hljs-comment">//const COLORS = ["#DC2626", "#D97706" ...</span>

<span class="hljs-comment">// Fetch location data at the start</span>
fetchLocation().then(<span class="hljs-function"><span class="hljs-params">location</span> =&gt;</span> {
  locationData = location;
});

<span class="hljs-comment">//const client = createClient({</span>
..
</code></pre>
<h3 id="heading-the-build-and-bundle-part">The build and bundle part</h3>
<p>Now we are going to build and bundle the code and its dependencies into a single JavaScript -file that can be used in OutSystems.</p>
<p>Let's add a folder called static. This is where our bundled file will go.</p>
<p><code>mkdir static</code></p>
<p>In the <strong><em>package.json</em></strong> remove the <em>main</em> property and change the <em>scripts</em> property so that we use <em>esbuild</em> bundle the file. It should look something like this</p>
<pre><code class="lang-json"> <span class="hljs-string">"name"</span>: <span class="hljs-string">"liveblocksdemo"</span>,
  <span class="hljs-string">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"esbuild app.ts --bundle --outfile=static/liveblocks.js"</span>
  },
  <span class="hljs-string">"keywords"</span>: [],
  <span class="hljs-string">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-string">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-string">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-string">"dependencies"</span>: {
    <span class="hljs-attr">"@liveblocks/client"</span>: <span class="hljs-string">"^2.4.0"</span>,
    <span class="hljs-attr">"esbuild"</span>: <span class="hljs-string">"^0.23.0"</span>
  }
}
</code></pre>
<p>now build and you will get a file <strong>static/liveblocks.js</strong></p>
<p><code>npm run build</code></p>
<h3 id="heading-the-low-code-part">The Low-Code part</h3>
<p>Lets move on to OutSystems Service Studio to integrate the code we have generated.</p>
<p>Start by setting up the app</p>
<ol>
<li><p>Create an App from scratch, and give it a name and logo</p>
</li>
<li><p>Create a <em>Module</em> as <em>Reactive Web App</em> and give it a name</p>
</li>
<li><p>Create a Blank Screen in the <em>MainFlow</em> of the Interface</p>
</li>
<li><p>Then we need to go to <strong>Interface  Scripts  import Script</strong> and choose the JavaScript bundle from the previous part, <strong>liveblocks.js</strong></p>
</li>
</ol>
<p>We then need to set up our UI. Let's put a simple <strong><em>Input Widget</em></strong> where users can provide their names. Assign a local Variable to the Input Field.</p>
<p>Name: <em>nameInput</em> (ref. eventListener in the code)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724231702528/d772d145-fdb2-4725-9bad-b87e8e848b2c.png" alt /></p>
<p>To display the cursors for each user, we will incorporate an I<strong>nline SVG Widget</strong> into our UI. SVGs are particularly useful when we want editable graphics, such as when we need to change the color of the cursors.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724231703929/d388b926-d761-4488-a0d7-01004150fdca.png" alt /></p>
<p>Heres the SVGCode for copy-paste</p>
<pre><code class="lang-xml">"<span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span><span class="hljs-attr">cursor</span>"" <span class="hljs-attr">id</span>=<span class="hljs-string">""</span><span class="hljs-attr">cursortemplate</span>"" <span class="hljs-attr">width</span>=<span class="hljs-string">""</span><span class="hljs-attr">24</span>"" <span class="hljs-attr">height</span>=<span class="hljs-string">""</span><span class="hljs-attr">36</span>"" <span class="hljs-attr">viewBox</span>=<span class="hljs-string">""</span><span class="hljs-attr">0</span> <span class="hljs-attr">0</span> <span class="hljs-attr">24</span> <span class="hljs-attr">36</span>"" <span class="hljs-attr">fill</span>=<span class="hljs-string">""</span><span class="hljs-attr">transparent</span>"" <span class="hljs-attr">xmlns</span>=<span class="hljs-string">""</span><span class="hljs-attr">http:</span>//<span class="hljs-attr">www.w3.org</span>/<span class="hljs-attr">2000</span>/<span class="hljs-attr">svg</span>""&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">""</span><span class="hljs-attr">M5.65376</span> <span class="hljs-attr">12.3673H5.46026L5.31717</span> <span class="hljs-attr">12.4976L0.500002</span> <span class="hljs-attr">16.8829L0.500002</span> <span class="hljs-attr">1.19841L11.7841</span> <span class="hljs-attr">12.3673H5.65376Z</span>""&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">path</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>"
</code></pre>
<p>Last we need to define a Container for the boundaries where cursors will collaborate. Give it the following properties</p>
<ul>
<li><p>Name: <em>cursorscontainer</em></p>
</li>
<li><p>Height: <em>600px</em></p>
</li>
<li><p>Width: <em>800px</em></p>
</li>
<li><p>Background Color: <em>{of your choice}</em></p>
</li>
</ul>
<p>To give it the Miro-like look and feel add these CSS elements to your app:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.cursor</span> {
  <span class="hljs-attribute">position</span>: absolute;
  <span class="hljs-attribute">top</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">left</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">opacity</span>: <span class="hljs-number">100%</span>;
}

<span class="hljs-selector-id">#cursorscontainer</span> {
  <span class="hljs-attribute">position</span>: relative;
  <span class="hljs-attribute">top</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">left</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">width</span>: <span class="hljs-number">100vw</span>;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">100vh</span>;
}

<span class="hljs-comment">/* Cursor Text */</span>
<span class="hljs-selector-class">.cursortext</span> {
  <span class="hljs-attribute">position</span>: absolute;
  <span class="hljs-attribute">width</span>: auto; <span class="hljs-comment">/* Auto width for dynamic text */</span>
  <span class="hljs-attribute">height</span>: auto; <span class="hljs-comment">/* Auto height for dynamic text */</span>
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#ffffff</span>; <span class="hljs-comment">/* White text color */</span>
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">5px</span> <span class="hljs-number">10px</span>; <span class="hljs-comment">/* Padding around the text */</span>
  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">10px</span>; <span class="hljs-comment">/* Rounded corners */</span>
  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">14px</span>; <span class="hljs-comment">/* Font size */</span>
  <span class="hljs-attribute">font-family</span>: Arial, sans-serif; <span class="hljs-comment">/* Font family */</span>
  <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0px</span> <span class="hljs-number">4px</span> <span class="hljs-number">10px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.2</span>); <span class="hljs-comment">/* Soft shadow for depth */</span>
  <span class="hljs-attribute">transition</span>: opacity <span class="hljs-number">0.2s</span> ease-in-out; <span class="hljs-comment">/* Smooth transition for opacity */</span>
  <span class="hljs-attribute">pointer-events</span>: none; <span class="hljs-comment">/* Prevent the cursor from interacting with the label */</span>
  <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>; <span class="hljs-comment">/* Initially hidden */</span>
  <span class="hljs-attribute">left</span>: <span class="hljs-number">10px</span>; <span class="hljs-comment">/* Position from the left */</span>
  <span class="hljs-attribute">top</span>: <span class="hljs-number">5px</span>;
  <span class="hljs-attribute">z-index</span>: <span class="hljs-number">1000</span>; <span class="hljs-comment">/* Ensure it appears above other elements */</span>
}
</code></pre>
<p>As a Final step before we publish, we need to add the JavaScript to run at the end of the Body when the page is loaded.</p>
<ul>
<li><p>Go to Screen and add <strong>OnReady</strong> event</p>
</li>
<li><p>In the Client Action <em>OnReady</em> a JavaScript widget with the following code ( remember to change src to your JavaScripts Runtime Path)</p>
</li>
</ul>
<pre><code class="lang-javascript">(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">var</span> script = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'script'</span>);
  script.src = <span class="hljs-string">'/LiveblockDemo/scripts/LiveblockDemo.liveblocks.js'</span>; <span class="hljs-comment">// Runtime Path from Script</span>
  <span class="hljs-built_in">document</span>.body.appendChild(script);
})();
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724231705208/1497c153-e610-4238-93fe-22a5c01439d4.png" alt /></p>
<p>NOW PUBLISH!</p>
<p>Open two browser windows side-by-side and see it in action. You should get something like this. Voil, multiplayer real-time cursors in OutSystems!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724231707120/7745e41f-113b-412f-87ca-073e72fd754c.gif" alt /></p>
<p>Example with two Clients, where one fetches location and one has name filled</p>
<p>Feel free to test out demo app <a target="_blank" href="https://personal-fik7fyn0.outsystemscloud.com/HelloMultiplayer/LiveblocksDemo"><strong>here</strong></a></p>
<h2 id="heading-the-final-thoughts-part">The Final thoughts part...</h2>
<p>In this tutorial, we only scratched the surface of whats possible with Liveblocks and its multiplayer features. For more in-depth information, please visit their <a target="_blank" href="http://liveblocks.io">website</a> and <a target="_blank" href="https://liveblocks.io/docs">documentation</a>. I know I will explore further as well.</p>
<p>I believe this tutorial provides a basic starting point for building a collaborative multiplayer web app on OutSystems. To make this app production-ready, we need to implement more secure authentication methods, modify the JavaScript code and OutSystems configurations to better integrate Rooms and Users through actions and data in OutSystems, and also ensure the cursor presence is responsive across different screen resolutions. I look forward to exploring this some more and how it can be used in different use cases.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724231708246/caaababc-be9e-4533-89c2-163a39d5ead3.png" alt /></p>
<p>Feel free to connect with me on <a target="_blank" href="https://www.linkedin.com/in/houman-maukon-mohebbi-1336692/">LinkedIn</a> to share thoughts</p>
]]></description><link>https://houman.hashnode.dev/how-to-create-a-multiplayer-web-app-in-outsystems-using-liveblocks</link><guid isPermaLink="true">https://houman.hashnode.dev/how-to-create-a-multiplayer-web-app-in-outsystems-using-liveblocks</guid><category><![CDATA[outsystems]]></category><category><![CDATA[liveblocks]]></category><category><![CDATA[Low Code]]></category><category><![CDATA[low code development]]></category><category><![CDATA[nocode]]></category><category><![CDATA[Collaboration]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Houman Mohebbi]]></dc:creator></item></channel></rss>