Package: djelm
DjelmElm integration for Django a.k.a. Django + Elm = 💚 DjelmTable Of ContentThe whyThe whenRequirementsElm setupDjango setupDjelm setupYour first Elm programcreate Commandaddprogram Commandnpm Commandwatch Commandcompile Commandcompilebuild CommandTemplate tagsFlagsFlag classesgeneratemodel CommandElm resourcesThe whyDjango 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.The whenBecause Djelm isnot intendedto 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.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.Try out a killer combo such asHTMXandAlpine 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.RequirementsElm 0.19.1Python >=3.11Django >= 4.2Node >= 16.4Elm setupDjelm will expect the Elm binary to be in yourPATH.Head on over to theinstallation guideto get the Elm binary on your
system.After installing, let's make sure Elm is ready to go. In your terminal run the command:elmYou should see a friendly welcome message and some other helpful Elm info.Django setupIf 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-mvenvvenvThen let's start the virtual environment:sourcevenv/bin/activateLet's get the Django package with pip:pipinstalldjangoCreate the django project:django-adminstartprojectdjelmproject&&cddjelmprojectLets look at whatstartprojectcreated:djelmproject
├──manage.py
├──djelmproject
│├──__init__.py
│├──settings.py
│├──urls.py
│├──asgi.py
│└──wsgi.pyDjelm setupYou will need thedjelmpackage, let's get it withpip.pipinstalldjelmAdd thedjelmapp to yourINSTALLED_APPSinsettings.py# settings.pyINSTALLED_APPS=[...,"djelm",]Optionally set your package manager of choice.[!NOTE]
If you don't set this variable then Djelm will try to usepnpm. Use
theinstall guideif you would like to use this default and you don't currently
havepnpminstalled.Wehighlyrecommend you usepnpmas it doesn't install node package peer dependencies, and usually provides a hassle free performant experience.# settings.pyNODE_PACKAGE_MANAGER="yarn"# npm, pnpm (default)Your first Elm programcreateCommandThe first thing we will need to do is create a directory where all your Elm programs will live. Djelm is fairly
opinionated about what lives inside this directory so for the best experience let's use thecreatecommand.From your Django project root:pythonmanage.pydjelmcreateelm_programs[!TIP]
Theelm_programsargument 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 theelm_programsapp.If we take a look at ourelm_programsapp 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
├──templates
│├──include.html
│└──elm_programs
└──templatetags└──__init__.pyWhat you are seeing is the directory structure required for Djelm to seamlessly work with both Django and Elm.Everything outside of thestatic_srcdirectory should look like a typical Django app, and everything inside
ofstatic_srcshould look like a conventional Elm project, with some extra bits.addprogramCommandNow that we have a place for Elm programs to live let's go ahead and add one!pythonmanage.pydjelmaddprogramelm_programsMain[!TIP]
You can change theMainargument to whatever makes the most sense for your program.
e.g.Map,TodoApp,UserProfile.
For the most predictable results when running theaddprogramcommand, ensure you use the
Elm module naming conventions which you can findhere.Looking at theelm_programsapp 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*
├──templates
│├──include.html
│└──elm_programs
│└──main.html*
└──templatetags├──__init__.py└──main_tags.py*Jump in to thestatic_src/src/Main.elmfile in theelm_programsdirectory 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 theMsgtype!typeMsg=Increment|DecrementnpmCommandTo 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 theelm_programspackage.jsonfile.[!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:pythonmanage.pydjelmnpmelm_programsinstall[!NOTE]
The above command runspnpm installin theelm_programs/static_srcdirectory.pnpmwill be substituted for
whatever is in theNODE_PACKAGE_MANAGERvariable insettings.py.alternatively you could do the following:cdelm_programs/static_src/&&pnpminstallMore generally, you can use any arguments you want for thenpmcommand after theelm_programsargument:# pnpm, yarnpythonmanage.pydjelmnpmelm_programsadd-Dsome-cool-npm-package# npmpythonmanage.pydjelmnpmelm_programsinstall-Dsome-cool-npm-packagewatchCommandAfter all node packages have installed we can use the Djelmwatchstrategy to compile our Elm programs and watch for
changes.pythonmanage.pydjelmwatchelm_programsYou should see some output like the following:Built2bundlesin100ms!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
├──templates
│├──include.html
│└──elm_programs
│└──main.html
└──templatetags├──__init__.py└──main_tags.pyDjelm compiled ourMain.elmprogram and bundled it up for us in a place where Django can work with it, awesome!compileCommandTo compile our programs as a one off command we can use the following:pythonmanage.pydjelmcompileelm_programscompilebuildCommandUse thecompilebuildcommand for generating production quality assets.From the command line run:pythonmanage.pydjelmcompilebuildelm_programs[!TIP]
After you have compiled your Elm programs for a production environment, it is advised that you
remove thestatic_srcdirectory 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 tagsLet's now actually render something in the browser by adding ourMainprograms tags in a Django template.[!NOTE]
I have added the followingbase.htmltemplate to theelm_programs/templatesdirectory for demonstration purposes.
If you already have a Django project you can just add the tags in whatever templates you want to render the Elm
program.<!-- base.html -->{% load static %}{% load static main_tags %}<!DOCTYPE html><htmllang="en"><head><title>Djelm</title><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/>{% include_main %}</head><body><h1>Django + Elm = ❤️</h1>{% render_main %}</body></html>Then we can point a url to the template file we just created.# urls.pyurlpatterns=[path("",TemplateView.as_view(template_name="base.html")),]We can now start the Django server.pythonmanage.pyrunserverHead on over tohttp://127.0.0.1:8000/and Voilà!Flags[!NOTE]
Flags are a critical paradigm in Elm so I recommend checking
outthese docsfor 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 of0, but0is
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 theelm_programs/templatetags/main_tags.pyfile that was generated for you:@register.inclusion_tag("elm_programs/main.html",takes_context=True)defrender_main(context):return{"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.This0value is set from therender_maintag 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.pydefcounter_view(request:AuthedHttpRequest):context={"counter_start":0,}returnrender(request,"base.html",context)The context that was set in the view is available to us from therender_maintag function which we can
callMainFlagswith:# main_tags.py@register.inclusion_tag("elm_programs/main.html",takes_context=True)defrender_main(context):return{"flags":MainFlags.parse(context["counter_start"])}Whilst in this example we are hardcoding a value of0, you really can pass it anyintyou want, perhaps an ID from a
database
model or some other computed value.The one constraint you have is that it must be anint, 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
docshere.Let's try passing something that isn't anintand see what happens:# main_tags.py@register.inclusion_tag("elm_programs/main.html",takes_context=True)defrender_main(context):return{"flags":MainFlags.parse("Hello Elm!")}What you should be seeing is a server error, but why? Let's find out!Flag classesIf we inspect theMainFlagsfunction inelm_programs/flags/main.pywe will see the following:MainFlags=Flags(IntFlag())TheFlagsclass takes as it's argument the flags shape that you want to pass to theMain.elmprogram
when it initializes.We are using theIntFlagclass which enforces that the type of value that can be passed to ourMain.elmprogram is
anint.
If we pass anything other than aninttoMainFlagsan error will be raised just like the one we saw when we used the
valueMainFlags.parse("hello Elm!").TheIntFlagclass is also directly related to the contract that exists in our Elm program as seen
inelm_programs/static_src/src/Models/Main.elm:typealiasToModel=InttoModel:Decode.DecoderToModeltoModel=Decode.intWhat this code reveals is that ourMain.elmprogram on initialization will only accept flags that are of typeInt. A
successful decoding of the flag to anIntwill thus produce a model ofIntwhich agrees perfectly with the python flag
definitions.You can think aboutIntFlagas a schema for your Elm program.What we end up with is a very strong contract for both the Django server and the Elm program. This workflow ensures that
data is being validated on both sides.The default program we generated takes this to the next level by forcing you to handle the possibility of the flag not being anInton
initialization as seen inelm_programs/static_src/src/Main.elm:init:Value->(Model,CmdMsg)initf=casedecodeValuetoModelfof-- Flag decoded to an Int, Success!Okm->(Readym,Cmd.none)-- Flag failed to decode to an Int, Error!Err_->(Error,Cmd.none)We get delicious 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.generatemodelCommandOur 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 theelm_programs/static_src/src/Models/Main.elmmodule 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 theIntFlagclass to something else for our default program:MainFlags=Flags(StringFlag())Then in the console run:pythonmanage.pydjelmgeneratemodelelm_programsMain[!NOTE]
If you have run thewatchcommand, Djelm will automatically detect flag changes and generate the models for you.Looking inelm_programs/static_src/src/Models/Main.elmyou can see our decoders have changed to handle aStringvalue:typealiasToModel=StringtoModel:Decode.DecoderToModeltoModel=Decode.stringWe 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 runningpython manage.py djelm generatemodel elm_programs Main:typealiasToModel={geometry:(List(ListFloat)),token:MaybeString,someObject:SomeObject_}typealiasSomeObject_={hello:String,elm:String}toModel:Decode.DecoderToModeltoModel=Decode.succeedToModel|>required"geometry"(Decode.list(Decode.listDecode.float))|>required"token"(Decode.nullableDecode.string)|>required"someObject"someObject_DecodersomeObject_Decoder:Decode.DecoderSomeObject_someObject_Decoder=Decode.succeedSomeObject_|>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 thecompilecommand and experience first hand
the beauty that is an Elm compiler message.Elm resourcesOfficial Elm siteYou can find a wonderful introduction to Elm, as well as all the information required to start writing elm programs.
There is also anonline editorto try out writing your own programs from scratch or from
example programs.Elm community SlackThe official place for Elm news, jobs, and genreal chit chat.Elm in actionBy 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 courseHere's that Richard Feldman guy again! Richard explores all the juicy bits of Elm in a very high quality presentation.Elm discourseJoin our wonderful and friendly Elm forum community! Ask questions, check out what people are working on, or just just
say hi!Incremental ElmThe most experienced and smartest of our community tend to hang in here. Extremely welcoming, and lots of great
channels to join.2023 ©Confidenceman02 - A Full Stack Django Developer and Elm shill