Skip to main content

A framework for using Elm programs in a Django project

Project description

ContourPy

Djelm

Actions status

[!NOTE] Djelm is an active project and prior to a stable release changes might come hard and fast as we explore and learn the best way to integrate Elm programs into Django.

Much effort is put into the CHANGELOG to describe and advise on all changes between versions so be sure to utilise it when updating to the latest and greatest version.

Elm integration for Django a.k.a. Django + Elm = ๐Ÿ’š


Table of Content

The why

Django is an awesome framework for rapidly building web apps, but it can be tricky to build UI's that require a level of reactivity unable to be expressed via templates alone.

Elm is a statically and strongly typed language with an approachable syntax that provides exceptional programming ergonomics to build highly reactive UI's. Elm programs are robust, reliable, and famously, delightful to write and maintain!

Djelm provides the bridge for both of these wonderful technologies to converge allowing you to seamlessly build the dynamic parts of your UI whilst working with the Django conventions you know and love.

Back to top

The when

Because djelm is not intended to be the primary UI solution for your Django project, the following guidelines serve as an initial checklist that will ensure your use of djelm is fit for purpose.

  1. Use the Django conventions and tools.

    • The Django ecosystem has many great libraries that can help you organise tricky UI for forms, tables, pagination, and search. You get the benefit of these tools not needing a UI framework.
  2. Try out a killer combo such as HTMX and Alpine JS.

    • This combo being able to handle a huge of amount of your UI reactivity is entirely conceivable, however, consider the following.
      • HTMX: Ensure your UI/UX won't suffer as a result of a roundtrip to the server.
      • Alpine JS: If you find yourself writing app logic akin to that of a framework, djelm is likely a far better option.

Back to top

Requirements

  • Elm 0.19.1
  • Python >=3.11
  • Django >= 4.2
  • Node >= 16.4

Back to top

Elm setup

Djelm will expect the Elm binary to be in your PATH.

Head on over to the installation guide to get the Elm binary on your system.

After installing, let's make sure Elm is ready to go. In your terminal run the command:

elm

You should see a friendly welcome message and some other helpful Elm info.

Back to top

Django setup

If you don't have a Django project ready to go, we will need to take care of some initial setup.

It's best practice to create a python virtual environment:

python -m venv venv

Then let's start the virtual environment:

source venv/bin/activate

Let's get the Django package with pip:

pip install django

Create the django project:

django-admin startproject djelmproject && cd djelmproject

Lets look at what startproject created:

djelmproject
โ”œโ”€โ”€ manage.py
โ”œโ”€โ”€ djelmproject
โ”‚ย ย  โ”œโ”€โ”€ __init__.py
โ”‚ย ย  โ”œโ”€โ”€ settings.py
โ”‚ย ย  โ”œโ”€โ”€ urls.py
โ”‚ย ย  โ”œโ”€โ”€ asgi.py
โ”‚   โ””โ”€โ”€ wsgi.py

Back to top

Djelm setup

You will need the djelm package, let's get it with pip.

pip install djelm

Add the djelm app to your INSTALLED_APPS in settings.py

# settings.py

INSTALLED_APPS = [
    ...,
    "djelm",
]

Optionally set your package manager of choice.

[!NOTE] If you don't set this variable then djelm will try to use pnpm. Use the install guide if you would like to use this default and you don't currently have pnpm installed.

We highly recommend you use pnpm as it doesn't install node package peer dependencies, and usually provides a hassle free performant experience.

# settings.py

NODE_PACKAGE_MANAGER = "yarn"  # npm, pnpm (default)

Back to top

Your first Elm program

create Command

The first thing we will need to do is create a directory where all our Elm programs will live. Djelm is fairly opinionated about what lives inside this directory so for the best experience let's use the create command.

From your Django project root:

python manage.py djelm create elm_programs

[!TIP] The elm_programs argument is just a name that I give the app that all my Elm programs live in, feel free to call it something else for your project. For the purposes of this tutorial I will refer to the elm_programs app.

If we take a look at our elm_programs app directory let's see what was created for us.

elm_programs
โ”œโ”€โ”€ apps.py
โ”œโ”€โ”€ elm_programs.djelm
โ”œโ”€โ”€ flags
โ”œโ”€โ”€ static
โ”‚ย ย  โ””โ”€โ”€ dist
โ”œโ”€โ”€ static_src
โ”‚ย ย  โ”œโ”€โ”€ .gitignore
โ”‚ย ย  โ”œโ”€โ”€ djelm_src
โ”‚ย ย  โ”œโ”€โ”€ elm.json
โ”‚ย ย  โ”œโ”€โ”€ package.json
โ”‚ย ย  โ””โ”€โ”€ src
โ”‚ย ย      โ””โ”€โ”€ Models
โ””โ”€โ”€ templatetags
    โ””โ”€โ”€ __init__.py

What you are seeing is the directory structure required for djelm to seamlessly work with both Django and Elm.

Everything outside of the static_src directory should look like a typical Django app, and everything inside of static_src should look like a conventional Elm project, with some extra bits.

addprogram Command

Now that we have a place for Elm programs to live let's go ahead and add one!

python manage.py djelm addprogram elm_programs Main

[!TIP] You can change the Main argument to whatever makes the most sense for your program, like Map, TodoApp, UserProfile.

For the most predictable results when running the addprogram command, ensure you use the Elm module naming conventions which you can find here.

Looking at the elm_programs app directory again we can see a few things have been added.

[!NOTE] Added files marked with *

elm_programs
โ”œโ”€โ”€ apps.py
โ”œโ”€โ”€ elm_programs.djelm
โ”œโ”€โ”€ flags
โ”‚ย ย  โ””โ”€โ”€ main.py *
โ”œโ”€โ”€ static
โ”‚ย ย  โ””โ”€โ”€ dist
โ”œโ”€โ”€ static_src
โ”‚ย ย  โ”œโ”€โ”€ .gitignore
โ”‚ย ย  โ”œโ”€โ”€ djelm_src
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Main.ts *
โ”‚ย ย  โ”œโ”€โ”€ elm.json
โ”‚ย ย  โ”œโ”€โ”€ package.json
โ”‚ย ย  โ””โ”€โ”€ src
โ”‚ย ย      โ”œโ”€โ”€ Main.elm *
โ”‚ย ย      โ””โ”€โ”€ Models
โ”‚ย ย          โ””โ”€โ”€ Main.elm *
โ””โ”€โ”€ templatetags
    โ”œโ”€โ”€ __init__.py
    โ””โ”€โ”€ main_tags.py *

Jump in to the static_src/src/Main.elm file in the elm_programs directory and what you will see is a simple Elm program. You might be able to work out what this program does just by looking at the Msg type!

type Msg
    = Increment
    | Decrement

npm Command

To actually run this Elm program with Django we will need to compile it, for that we will need to install the node packages defined in the elm_programs package.json file.

[!NOTE] Elm doesn't actually need node to compile programs. However, djelm optimzes Elm programs to work with Django templates so a tiny amount of DOM binding code is bundled in.

We can install all node packages with the following command:

python manage.py djelm npm elm_programs install

[!NOTE] The above command runs pnpm install in the elm_programs/static_src directory. pnpm will be substituted for whatever is in the NODE_PACKAGE_MANAGER variable in settings.py.

alternatively you could do the following:

cd elm_programs/static_src/ && pnpm install

More generally, you can use any arguments you want for the npm command after the elm_programs argument:

# pnpm, yarn

python manage.py djelm npm elm_programs add -D some-cool-npm-package

# npm

python manage.py djelm npm elm_programs install -D some-cool-npm-package

watch Command

After all node packages have installed we can use the djelm watch strategy to compile our Elm programs and watch for changes.

python manage.py djelm watch elm_programs

You should see some output like the following:

 Built 2 bundles in 100ms!

Let's take a look at what changed.

elm_programs
โ”œโ”€โ”€ apps.py
โ”œโ”€โ”€ elm_programs.djelm
โ”œโ”€โ”€ flags
โ”‚ย ย  โ””โ”€โ”€ main.py
โ”œโ”€โ”€ static
โ”‚ย ย  โ”œโ”€โ”€ dist
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Main.47ea7fa8.js *
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Main.47ea7fa8.js.map *
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Main.js *
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Main.js.map *
โ”œโ”€โ”€ static_src
โ”‚ย ย  โ”œโ”€โ”€ .gitignore
โ”‚ย ย  โ”œโ”€โ”€ djelm_src
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ Main.ts
โ”‚ย ย  โ”œโ”€โ”€ elm.json
โ”‚ย ย  โ”œโ”€โ”€ package.json
โ”‚ย ย  โ””โ”€โ”€ src
โ”‚ย ย      โ”œโ”€โ”€ Main.elm
โ”‚ย ย      โ””โ”€โ”€ Models
โ”‚ย ย          โ””โ”€โ”€ Main.elm
โ””โ”€โ”€ templatetags
    โ”œโ”€โ”€ __init__.py
    โ””โ”€โ”€ main_tags.py

Djelm compiled our Main.elm program and bundled it up for us in a place where Django can work with it, awesome!

compile Command

To compile our programs as a one off command we can use the following:

python manage.py djelm compile elm_programs

compilebuild Command

Use the compilebuild command for generating production quality assets.

From the command line run:

python manage.py djelm compilebuild elm_programs

[!TIP] After you have compiled your Elm programs for a production environment, it is advised that you remove the static_src directory as it will contain cache files and node modules that you probably don't want sitting on your Django production server taking up space.

Template tags

Let's now actually render something in the browser by adding our Main programs tags to a Django template.

[!NOTE] I have added the following base.html and main.html templates to a elm_programs/templates directory for demonstration purposes. You will need to create this directory if you havent done so already.

If you already have a django project you can just add the tags into whatever templates you want to render the elm program.

<!-- base.html -->

<!doctype html>
<html lang="en">
  <head>
    <title>Djelm</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    {% block head %}{% endblock %}
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>
<!-- main.html -->

{% extends "base.html" %}
{% load static %}
{% load static main_tags %}
{% block head %}
    {% include_main %}
{% endblock %}
{% block content %}
    <h1>Django + Elm = โค๏ธ</h1>
    {% render_main %}
{% endblock %}

Then we can point a url to the main.html template file.

# urls.py
from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    path("", TemplateView.as_view(template_name="main.html")),
]

We can now start the Django server.

python manage.py runserver

Head on over to http://127.0.0.1:8000/ and Voilร !

example

Flags

[!NOTE] Flags are a critical paradigm in Elm so I recommend checking out these docs for a great introduction to them.

Flags are the way we pipe data from our Django server in to our Elm programs. They very much define the contract between python values and an Elm model.

Let's analyze the default program we generated earlier which simply increments and decrements an integer.

You will notice if we look at the running program in the browser that it starts off with an integer of 0, but 0 is not hardcoded anywhere in the Elm program. So where does it come from?

This value actually comes bound to our render tag when we server render the template, and is passed to the Elm program when it is initialized on the client, as a flag value.

Check out the elm_programs/templatetags/main_tags.py file that was generated for you:

@register.inclusion_tag("djelm/program.html", takes_context=True)
def render_main(context):
    return {"key": key, "flags": MainFlags.parse(0)}

Those experienced with Django might be having an 'Aha!' moment right now but don't worry if thats not the case, I'll explain everything.

This 0 value is set from the render_main tag function as a default, the actual values you will want to pass your Elm programs are much more likely to originate in your Django views like the following example:

# views.py

def counter_view(request: AuthedHttpRequest):
    context = {
        "counter_start": 0,
    }

    return render(request, "base.html", context)

The context that was set in the view is available to us from the render_main tag function which we can call MainFlags with:

# main_tags.py

@register.inclusion_tag("djelm/program.html", takes_context=True)
def render_main(context):
    return {"key": key, "flags": MainFlags.parse(context["counter_start"])}

Whilst in this example we are hardcoding a value of 0, you really can pass it any int you want, perhaps an ID from a database model or some other computed value.

The one constraint you have is that it must be an int, which is perfect for our default Elm program, but not very flexible for other types of Elm programs you might like to build.

[!NOTE] If you are not sure what Django tags are check out the docs here.

Let's try passing something that isn't an int and see what happens:

# main_tags.py

@register.inclusion_tag("djelm/program.html", takes_context=True)
def render_main(context):
    return {"key": key, "flags": MainFlags.parse("Hello Elm!")}

What you should be seeing is a server error, but why? Let's find out!

Flag classes

If we inspect the MainFlags function in elm_programs/flags/main.py we will see the following:

MainFlags = Flags(IntFlag())

The Flags class takes as it's argument the flags shape that you want to pass to the Main.elm program when it initializes.

We are using the IntFlag class which enforces that the type of value that can be passed to our Main.elm program is an int. If we pass anything other than an int to MainFlags.parse an error will be raised just like the one we saw when we used the value MainFlags.parse("hello Elm!").

The IntFlag class is intrinsic to the model that exists in our Main.elm program as seen in elm_programs/static_src/src/Models/Main.elm:

type alias ToModel =
    Int


toModel : Decode.Decoder ToModel
toModel =
    Decode.int

The Main.elm program uses this model to protect us from any uninvited flags that might show up at runtime as seen in elm_programs/static_src/src/Main.elm:

init : Value -> ( Model, Cmd Msg )
init f =
    case decodeValue toModel f of
        Ok m -> -- Flag decoded to an Int, Success!
            ( Ready m, Cmd.none )

        Err _ -> -- Flag failed to decode to an Int, Error!
            ( Error, Cmd.none )

To summarize, we get compile and runtime guarantees that our program will behave as we expect. This is what makes Elm programs incredibly robust and nearly impossible to produce a runtime exception with.

You can check out all the python flags you can use to express your data shape here.

generatemodel Command

Our python flag classes have a little trick up their sleeve that makes keeping both our python and Elm flag contracts in sync for us.

We can see that in the elm_programs/static_src/src/Models/Main.elm module we have the following comment:

{-| Do not manually edit this file, it was auto-generated by djelm
<https://github.com/Confidenceman02/django-elm>
-}

That's right! Djelm makes it uneccessary to ever need to write your own flag decoders, which can be a hefty task if the shape of your flags is complicated. Whatever we define as our flags shape in Python will determine the decoders we find in our Elm program.

Let's put this to the test and change the IntFlag class to something else for our default program:

MainFlags = Flags(StringFlag())

Then in the console run:

python manage.py djelm generatemodel elm_programs Main

[!NOTE] If you have run the watch command, djelm will automatically detect flag changes and generate the models for you.

Looking in elm_programs/static_src/src/Models/Main.elm you can see our decoders have changed to handle a String value:

type alias ToModel =
    String


toModel : Decode.Decoder ToModel
toModel =
    Decode.string

We can get more adventurous to really see the heavy lifting djelm is doing for us:

MainFlags = Flags(
    ObjectFlag(
        {
            "geometry": ListFlag(ListFlag(FloatFlag())),
            "token": NullableFlag(StringFlag()),
            "someObject": ObjectFlag({"hello": StringFlag(), "elm": StringFlag()}),
        }
    )
)

And the result of running python manage.py djelm generatemodel elm_programs Main:

type alias ToModel =
    { geometry : (List (List Float))
    , token : Maybe String
    , someObject : SomeObject_
    }

type alias SomeObject_ =
    { hello : String
    , elm : String
    }


toModel : Decode.Decoder ToModel
toModel =
    Decode.succeed ToModel
        |>  required "geometry" (Decode.list (Decode.list Decode.float))
        |>  required "token" (Decode.nullable Decode.string)
        |>  required "someObject" someObject_Decoder

someObject_Decoder : Decode.Decoder SomeObject_
someObject_Decoder =
    Decode.succeed SomeObject_
        |>  required "hello" Decode.string
        |>  required "elm" Decode.string

[!TIP] The default Elm program we generated will fail to compile when performing the model changes above.

To help you understand why it failed you can run the compile command and experience first hand the beauty that is an Elm compiler message.

Widgets

Djelm widgets are feature rich, highly dynamic and customizable programs we can use to supercharge our UI's.

The widgets are added and live inside your app similar to that of a regular program that was added with the addprogram command. The key difference being that they are purpose built, handsomely styled, and ready to be used straight out of the box.

Let's consider this simple model and form.

# models.py
from django.db import models

class Course(models.Model):
    name = models.CharField(max_length=100)
    instructor = models.CharField(max_length=100)

    def __str__(self) -> str:
        return self.name


class Promotion(models.Model):
    courses = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True)

# forms.py
from django import forms

class PromotionForm(forms.ModelForm):
    courses = forms.ModelChoiceField(
        queryset=Course.objects.all(),
    )

    class Meta:
        model = Promotion
        fields = ["courses"]

Django will conveniently render a completely usable UI with very little effort from us.

example

It's a great start but we want something more customizable, and a fair bit fancier. It's widget time!

listwidgets Command

Let's see a list of all the djelm widgets

python manage.py djelm listwidgets

The ModelChoiceField widget from this list seems like the one we want.

addwidget Command

Let's add the widget to our elm_programs app.

python manage.py djelm addwidget elm_programs ModelChoiceField

We will also need to compile the widget program before we can use it.

python manage.py djelm compile elm_programs

[!NOTE] If you have run the watch command, djelm will automatically compile the program for you.

Now that we have a widget program to render let's set a custom template for our form field.

# forms.py
from django import forms

class PromotionForm(forms.ModelForm):
    courses = forms.ModelChoiceField(
        queryset=Course.objects.all(),
        template_name="modelChoiceFieldWidget.html", # <--- Added
    )

    class Meta:
        model = Promotion
        fields = ["courses"]

Let's now create that custom template.

<!-- modelChoiceFieldWidget.html -->

{% extends "base.html" %}
{% load modelChoiceField_widget_tags %}
{% block head %}
    {% include_ModelChoiceFieldWidget %}
{% endblock %}
{% block content %}
    {{ field.label_tag }}
    {% render_ModelChoiceFieldWidget %}
{% endblock %}

Now that's pretty handsome!

example

We get a nice style and feature improvement over the default input Django gives us, but can we do better?

Yes we can!

Because widget programs are just normal Elm programs that live inside your djelm app, you have complete control to customize them as you see fit should they not quite fit your branding or presentational requirements.

Let's delight our users by giving them more information about their choices.

example

That's a good looking widget!

[!NOTE] For specific documentation and advanced features for widgets, check out the widget documentation.

Back to top

Elm resources

  • Official Elm site

    You can find a wonderful introduction to Elm, as well as all the information required to start writing elm programs. There is also an online editor to try out writing your own programs from scratch or from example programs.

  • Elm community Slack

    The official place for Elm news, jobs, and genreal chit chat.

  • Elm in action

    By far the most comprehensive book on Elm that exists, written by the mighty Richard Feldman, he will take you from beginner to expert.

  • Front end Masters course

    Here's that Richard Feldman guy again! Richard explores all the juicy bits of Elm in a very high quality presentation.

  • Elm discourse

    Join our wonderful and friendly Elm forum community! Ask questions, check out what people are working on, or just just say hi!

  • Incremental Elm

    The most experienced and smartest of our community tend to hang in here. Extremely welcoming, and lots of great channels to join.

Back to top

2023 ยฉ Confidenceman02 - A Full Stack Django Developer and Elm shill

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

djelm-0.10.0.tar.gz (52.6 kB view hashes)

Uploaded Source

Built Distribution

djelm-0.10.0-py3-none-any.whl (60.4 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page